在Haskell中的函数之间共享变量的惯用方式?

pap*_*uck 10 haskell

我遇到一种情况,其中递归函数根据命令行参数做出决定。递归函数不会直接由调用main。我想知道最好的方法是使参数对函数可用。我不想getArgs在递归函数中调用,因为这似乎会增加很多开销。

然而,尴尬的来电getArgsmain,然后通过不使用这些函数传递参数。这个例子不是递归的,但是希望您能理解这个概念。

import Data.Char
import System.Environment

main :: IO ()
main = do
    args <- getArgs  -- want to use these args in fun2
    traverse_ fun1 ["one", "two", "three"]

fun1 :: String -> IO ()
fun1 s = traverse_ fun2 s

fun2 :: Char -> IO ()
fun2 c = do
    if "-u" `elem` args then print $ toUpper c  -- how to get args here?
    else print $ toLower c
Run Code Online (Sandbox Code Playgroud)

传递参数似乎是一个坏主意:

import Data.Char
import System.Environment

main :: IO ()
main = do
    args <- getArgs -- want to use these args in fun2
    traverse_ (fun1 args) ["one", "two", "three"]

fun1 :: [String] -> String -> IO ()
fun1 args s = traverse_ (fun2 args) s

fun2 :: [String] -> Char -> IO ()
fun2 args c = do
    if "-u" `elem` args then print $ toUpper c
    else print $ toLower c
Run Code Online (Sandbox Code Playgroud)

在面向对象的语言中,您将只在一个类中拥有一个成员变量,或者某种全局变量。

typ*_*ern 14

将参数传递给fun1没有什么尴尬-它确实使用了它们(将参数传递给func2 就是在使用它们)。

什么尴尬的,是让你FUN1或FUN2的行为依赖于隐变量,使他们的行为难以推理或预测。

您可以做的另一件事:将fun2设为fun1的参数(您可以在Haskell中将函数作为参数传递!):

fun1 :: (Char -> IO ()) -> String -> IO ()
fun1 f s = traverse_ f s
Run Code Online (Sandbox Code Playgroud)

然后,您可以main像这样调用它:

traverse_ (fun1 (fun2 args)) ["one", "two", "three"]
Run Code Online (Sandbox Code Playgroud)

这样,您可以将参数直接传递给fun2,然后将fun2传递给fun1 ...


che*_*ner 8

对于情况下,当你真的需要一个共享的,只读的环境中,使用的Reader单子,或在这种情况下,ReaderT单子转换。

import Data.Char
import Data.Foldable
import System.Environment
import Control.Monad.Trans
import Control.Monad.Trans.Reader

main :: IO ()
main = do
    args <- getArgs
    -- Pass in the arguments using runReaderT
    runReaderT (traverse_ fun1 ["one", "two", "three"]) args

-- The type changes, but the body stays the same.
-- fun1 doesn't care about the environment, and fun2
-- is still a Kleisli arrow; traverse_ doesn't care if
-- its type is Char -> IO () or Char -> ReaderT [String] IO ()
fun1 :: String -> ReaderT [String] IO ()
fun1 s = traverse_ fun2 s

-- Get the arguments using ask, and use liftIO
-- to lift the IO () value produced by print
-- into monad created by ReaderT
fun2 :: Char -> ReaderT [String] IO ()
fun2 c = do
    args <- ask
    liftIO $ if "-u" `elem` args 
      then print $ toUpper c
      else print $ toLower c
Run Code Online (Sandbox Code Playgroud)

顺便说一句,您可以fun2稍微重构:

fun2 :: Char -> ReaderT [String] IO ()
fun2 c = do
    args <- ask
    let f = if "-u" `elem` args then toUpper else toLower
    liftIO $ print (f c)
Run Code Online (Sandbox Code Playgroud)

事实上,你可以选择toUpper或者toLower只要你的论点,并把那个,而不是参数本身,在环境中。

main :: IO ()
main = do
    args <- getArgs
    -- Pass in the arguments using runReaderT
    runReaderT 
      (traverse_ fun1 ["one", "two", "three"])
      (if "-u" `elem` args then toUpper else toLower)

fun1 :: String -> ReaderT (Char -> Char) IO ()
fun1 s = traverse_ fun2 s

fun2 :: Char -> ReaderT (Char -> Char) IO ()
fun2 c = do
    f <- ask
    liftIO $ print (f c)
Run Code Online (Sandbox Code Playgroud)

环境类型可以是任何值。上面的示例显示了一个字符串列表和一个Char -> Char作为环境的字符串。通常,您可能需要一种自定义产品类型,该产品类型应包含要与其余代码共享的任何值,例如,

data MyAppConfig = MyAppConfig { foo :: Int
                               , bar :: Char -> Char
                               , baz :: [Strings]
                               }

main :: IO ()
main = do
    args <- getArgs
    -- Process arguments and define a value of type MyAppConfig
    runReaderT fun1 MyAppConfig

fun1 :: ReaderT MyAppConfig IO ()
fun1 = do
   (MyAppConfig x y z) <- ask  -- Get the entire environment and unpack it
   x' <- asks foo  -- Ask for a specific piece of the environment
   ...
Run Code Online (Sandbox Code Playgroud)

您可能需要阅读有关ReaderT设计模式的更多信息。


Mar*_*ann 7

尽管typedfern的答案很好(建议),但编写尽可能多的纯函数,然后将效果推迟到您不能再推迟它们的时候,甚至会更加惯用。这使您可以创建数据管道,而不必传递参数。

我知道OP中显示的示例问题已经简化,可能只是微不足道的程度,但是如果将逻辑与其作用分开,则编写起来会更容易。

首先,重写fun2为纯函数:

fun2 :: Foldable t => t String -> Char -> Char
fun2 args c =
  if "-u" `elem` args then toUpper c else toLower c
Run Code Online (Sandbox Code Playgroud)

如果部分应用fun2参数,则您有一个类型为的函数Char -> Char。但是,["one", "two", "three"]您想要的数据()print具有类型[[Char]]。您想将每个Char值应用于fun2 args。本质上,这就是OP fun1函数的作用。

但是,您可以改为使用(或)将[[Char]]值展平。[Char]joinconcat

*Q56438055> join ["one", "two", "three"]
"onetwothree"
Run Code Online (Sandbox Code Playgroud)

现在,您只需将展Char平列表中的每个值应用于fun2 args

*Q56438055> args = ["-u"]
*Q56438055> fmap (fun2 args) $ join ["one", "two", "three"]
"ONETWOTHREE"
Run Code Online (Sandbox Code Playgroud)

这仍然是纯粹的结果,但是您现在可以通过打印每个字符来应用效果:

main :: IO ()
main = do
  args <- getArgs
  mapM_ print $ fmap (fun2 args) $ join ["one", "two", "three"]
Run Code Online (Sandbox Code Playgroud)

通过更改函数设计,以便在函数之间传递数据,通常可以简化代码。