在Haskell中实现函数间共享变量的惯用方式是什么?

9
我有这样一个情况,递归函数基于命令行参数做出决策。递归函数不是由main直接调用的。我想知道最好的方法是将这些参数传递给函数。我不想在递归函数内部调用getArgs,因为那会增加很多开销。
但是,在main中调用getArgs然后通过一个不使用这些参数的函数传递参数是很麻烦的。虽然这个例子不是递归的,但是希望你可以理解概念。
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

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

1
这听起来像是配置问题;请参考http://okmij.org/ftp/Haskell/tr-15-04.pdf以及其现代实现[Data.Reflection](https://hackage.haskell.org/package/reflection)(警告:高级黑魔法)。 - melpomene
我建议只需将上下文状态作为参数传递,但如果你真的想要,可以看一下 GHC 的 ImplicitParams 扩展。 - DarthFennec
2
在面向对象的语言中,你只需要在类中拥有一个成员变量或某种全局变量。如果成员是常量,则Haskell的惯用答案是ReaderT,如果成员需要可变,则为StateT。下面的其中一个答案会更详细地涵盖这一点。 - Asa
3个回答

13

向fun1传递参数并将它们传递给func2是很自然的事情(将它们传递给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...


7

在确实需要共享只读环境的情况下,请使用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

顺便说一下,您可以稍微重构一下 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,并将其放入环境中,而不是直接使用参数本身。

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设计模式的内容。


6
尽管 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的函数。但是,您希望print的数据(["one", "two", "three"])的类型为[[Char]]。您想将每个Char值应用于fun2 args。这基本上就是OP fun1函数所做的事情。
但是,您可以使用join(或concat)将[[Char]]值平铺为[Char]
*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"]

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接