【问题标题】:Using parameters that don't change after being read使用读取后不改变的参数
【发布时间】:2014-03-22 04:17:24
【问题描述】:

我正在学习 Haskell 并编写一个程序来解决一个玩具问题。该程序使用的参数k 在运行时不会更改,在从文件中读取参数之后。我对使用纯函数很陌生,我想尽可能多地编写纯函数。

我有一个数据类型Node,以及比较节点、获取节点后代等的函数。目前,所有这些函数都将参数k作为参数,例如

compare k node1 node2 = ...
desc k node = ...

每当我必须在函数中递归调用其中任何一个时,我都必须重复 k 参数。这似乎是多余的,因为 k 对这些函数永远不会有不同的值,而且它会降低类型签名的可读性,如果可能的话,我想重构它。

有没有什么策略可以用纯函数做到这一点,或者这只是我必须处理的限制?

我的想法

之前我在顶层对 k 进行了硬编码,它似乎可以工作(我能够在函数中使用 k 而无需将其作为显式参数)。但是,一旦我需要从文件中读取输入,这显然是不可行的。

另一种可能的策略是在 main 函数中定义所有这些函数,但在 Haskell 中似乎强烈反对这样做。

【问题讨论】:

  • 您需要k 做什么?除非它是诸如自定义比较函数之类的东西,否则节点之间的比较依赖于除节点本身之外的任何内容,这听起来有点奇怪。
  • @duplode 我使用k 来计算desc 中一个节点的后代,我在递归调用compare 时使用它,如果这有意义的话
  • 这很有道理。这些名字让我想到了影响结果的额外参数听起来很可疑的上下文(compare 标准函数,树中的节点),但这可能只是我。无论如何,关于纯度的一个好处是签名倾向于更好地反映实际的依赖关系(例如,从desc 的类型来看,结果可能取决于k 的值)。

标签: haskell monads purely-functional pure-function


【解决方案1】:

通常的 Haskell 方法是使用 Reader monad。对Reader 的一种思考方式是它为计算提供了对环境的访问。可以定义为

newtype Reader r a = Reader { runReader :: r -> a }

所以你的函数会有类型

compare :: Node -> Node -> Reader k Ordering -- or Bool, or whatever the return value is

desc :: Node -> Reader k String -- again, guessing at the output type.

Reader 计算中,使用函数ask :: Reader r r 来访问参数。

在顶层,您可以使用runReader theComputation env 运行Reader 计算

这通常比显式传递参数更好。首先,任何不需要环境的函数都可以写成普通函数而不用作为参数。如果它调用了另一个使用环境的函数,monad 将自动提供它,而无需您做任何额外的工作。

你甚至可以定义一个类型的同义词,

type MyEnv = Reader Env

并将其用于您的函数类型签名。那么如果你需要改变环境,你只需要改变一种类型,而不是改变你所有的类型签名。

standard libraries 的定义处理 monad 转换器有点复杂,但它的工作方式与这个更简单的版本相同。

【讨论】:

    【解决方案2】:

    最终,您必须在需要的任何地方传递 k 的值,但您可以采取一些措施来避免重复。

    您可以做的一件事是在知道k 的值后定义便利函数:

    myfunc = let k = ...
                 compare' = compare k
                 desc'    = desc k
             in ...
                (use compare' and desc' here)
    

    另一种方法是使用Implicit Parameters 扩展名。这涉及定义comparedesc 以将k 作为隐式参数:

    {-# LANGUAGE ImplicitParameters #-}
    
    compare :: (?k :: Int) => Node -> Node
    compare n1 n2 = ... (can use ?k here) ...
    
    desc :: (?k :: Int) => Node
    desc = ... (can use ?k here) ...
    
    myfunc = let ?k = ...
             in ... use compare and desc ...
    

    请注意,在任何一种情况下,您都不能调用 comparedesc,直到您定义了 k 是什么。

    【讨论】:

    • +1,尽管有些人可能不喜欢它,但我首先想到的是一个参数(在动态意义上)。
    • @Wes 为什么不喜欢它?只是不太像 Haskell?
    • @BirdJaguarIV 动态绑定是您在 lisp 家族的语言中发现的东西,可以说它并不真正“适合”Haskell,但 imo 它有它的用途。
    【解决方案3】:

    这就是我喜欢用不变的值构造递归函数的方式

    map f xs = map' xs
      where map' (x:xs) = f x : map' xs
    

    【讨论】:

      【解决方案4】:

      两个可能有用的局部函数定义的简单技巧。首先,您可以通过使用范围使k 隐含在您的递归定义中:

      desc :: Int -> Node -> [Node]
      desc k node = desc' node
          where
          desc' node = -- Carry on; k is in scope now.
      

      其次,如果您要在同一范围内使用相同的k 多次调用您的函数,您可以为 部分应用 函数使用本地定义:

      main = do
          k <- getKFromFile
          -- etc.
          let desc' = desc k -- Partial application.
              descA = desc' nodeA
              descB = desc' nodeB
          print [descA, descB]
      

      正确的隐式参数传递通常使用 Reader monad 完成(或者可以说是模拟的)(请参阅 John L 的回答),尽管这对于您的用例来说听起来有点重量级。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-02-09
        • 1970-01-01
        • 1970-01-01
        • 2011-04-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多