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

时间:2019-06-04 05:55:45

标签: haskell

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

但是,在getArgs中调用main然后通过不使用参数的函数传递参数是很尴尬的。这个例子不是递归的,但是希望您能理解这个概念。

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

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

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

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

3 个答案:

答案 0 :(得分:14)

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

的尴尬之处在于让您的fun1或fun2的行为取决于隐藏变量,使它们的行为难以推理或预测。

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

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

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

traverse_ (fun1 (fun2 args)) ["one", "two", "three"]

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

答案 1 :(得分:8)

对于确实需要共享的只读环境的情况,请使用Reader monad,在这种情况下,请使用ReaderT monad转换器。

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

顺便说一句,您可以稍微重构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)

实际上,您可以在获取参数后立即选择toUppertoLower,然后将那个而不是参数本身放到环境中。 / p>

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)

环境类型可以是任何值。上面的示例显示了一个字符串列表和一个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
   ...

您可能想详细了解ReaderT design pattern

答案 2 :(得分:7)

虽然 typedfern 的答案很好(建议),但编写尽可能多的纯函数然后推迟效果,直到无法再将其推迟为止。这样,您就可以创建数据管道,而不必传递参数。

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

首先,将fun2重写为纯函数:

fun2 :: Foldable t => t String -> Char -> Char
fun2 args c =
  if "-u" `elem` args then toUpper c else toLower c

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

但是,您可以使用[[Char]](或[Char])将join的值展平为concat

*Q56438055> join ["one", "two", "three"]
"onetwothree"

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

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

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

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

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