【问题标题】:Does the expressiveness of monads come at the expense of code reuse?monad 的表现力是以牺牲代码重用为代价的吗?
【发布时间】:2017-03-27 04:58:57
【问题描述】:

当我比较 Applicative 和 Monad 类型类的二元运算时

(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(=<<) :: Monad m       => (a -> m b) -> m a -> m b

两个区别变得明显:

  • ap 需要一个普通的纯函数,而bind 需要一个单子动作,它必须返回一个相同类型的单子
  • ap 的动作顺序由应用程序决定,而bind 的一元动作可以决定控制流

所以单子给了我额外的表达能力。然而,由于它们不再接受普通的纯函数,这种表现力似乎是以牺牲代码重用为代价的。

我的结论可能有点幼稚甚至是错误的,因为我对 Haskell 和 monadic 计算几乎没有经验。黑暗中的任何光明都值得赞赏!

【问题讨论】:

  • 你说的“代码重用”是什么意思? Monads 和 Applicatives 是在不同上下文中使用的不同概念。虽然每个 Monad 也是一个应用程序,但使用应用程序可以生成更高效的代码,例如 Parsers。
  • 有了仿函数,我可以在特定的计算上下文中重用普通的纯函数。使用应用程序,我可以重用从同一个应用程序的多个上下文中获取值的普通纯函数。现在我想在单子计算中重用普通的纯函数。但是bind 需要一个“特殊”函数(我称之为单子动作),它必须返回一个相同类型的单子。所以我的印象是,纯函数的重用并不是 monad 的目的。
  • 我无法理解前提。 ap 不采用函数 a -&gt; b,但 f (a -&gt; b) 是另一回事。当然,您可以使用ap (pure g) ...,但那是另一回事。此外,每个 monad 都是一个应用程序,所以如果你可以将类型 T a 变成一个 monad,那么没有理由不把它变成一个应用程序(事实上,这在最近的 GHC 中是强制性的......)因此,我不能理解这个问题。
  • @ftor (1) f (a -&gt; b) 值不一定是上下文中的单个纯函数——例如,考虑[(2*), (3+)] :: Num a =&gt; [a -&gt; a]Nothing :: Maybe (String -&gt; String)。虽然它确实由上下文中的(零个或多个)普通函数组成,但称其为“纯函数”会导致混淆。 (2)(=&lt;&lt;) :: Monad m =&gt; (a -&gt; m b) -&gt; m a -&gt; m b 的一个关键特性是m b 的上下文取决于a 值。在putStrLn =&lt;&lt; getLine 中,屏幕上显示的字符串与您在终端中键入的字符串完全相同。 fmap(&lt;*&gt;) 不能这样做。

标签: haskell functional-programming monads dry reusability


【解决方案1】:

如果您有纯函数g :: a -&gt; b,您可以将其设置为Applicative 版本

pure g :: Applicative f =&gt; f (a -&gt; b)

Monadish by

pure . g :: Applicative f =&gt; a -&gt; f b

因此,您不会失去任何代码重用。

【讨论】:

    【解决方案2】:

    只有当您可以重用代码来做您真正想做的事情时,代码重用才是一种优势。

    GHCi> putStrLn =<< getLine
    Sulfur
    Sulfur
    GHCi> 
    

    在这里,(=&lt;&lt;) 选择getLineIO 上下文中生成的String 结果并将其提供给putStrLn,然后它会打印所述结果。

    GHCi> :t getLine
    getLine :: IO String
    GHCi> :t putStrLn
    putStrLn :: String -> IO ()
    GHCi> :t putStrLn =<< getLine
    putStrLn =<< getLine :: IO ()
    

    现在,类型为fmap/(&lt;$&gt;)...

    GHCi> :t (<$>)
    (<$>) :: Functor f => (a -> b) -> f a -> f b
    

    ...b 完全有可能成为IO (),因此没有什么能阻止我们使用putStrLn。不过……

    GHCi> putStrLn <$> getLine
    Sulfur
    GHCi> 
    

    ...不会打印任何内容。

    GHCi> :t putStrLn <$> getLine
    putStrLn <$> getLine :: IO (IO ())
    

    执行IO (IO ()) 操作不会执行内部IO () 操作。为此,我们需要Monad 的额外功能,可以通过将(&lt;$&gt;) 替换为(=&lt;&lt;),或者等效地在IO (IO ()) 值上使用join

    GHCi> :t join
    join :: Monad m => m (m a) -> m a
    GHCi> join (putStrLn <$> getLine)
    Sulfur
    Sulfur
    GHCi> 
    

    和 chi 一样,我也很难理解你的问题的前提。您似乎期望FunctorApplicativeMonad 中的一个会比其他的更好。事实并非如此。与Functor 相比,我们可以用Applicative 做更多的事情,甚至用Monad 做更多的事情。如果您需要额外的力量,请使用适当强大的类。如果不是这样,使用不太强大的类将导致更简单、更清晰的代码和更广泛的可用实例。

    【讨论】:

    • 我想我对 monads 的理解太简单了。我只是想更好地了解如何以惯用的方式使用 (a -> m b)。很抱歉造成混乱,感谢您的帮助!
    【解决方案3】:

    这个问题的原因是我对函子、应用程序和 monad 类之间的关系的理解过于简略。我以为这只是关于重用纯函数。

    (&lt;*&gt;) 本质上说给我一个函数的应用程序和一堆值的应用程序,我会根据我的规则应用它们。

    (=&lt;&lt;) 本质上说给我一个函数(a -&gt; m b) 和一个monad,我将给(a -&gt; m b) 提供monad 的值并将其留给(a -&gt; m b) 以生成并返回包装在同一个monad 中的转换后的值.

    当然,applicative 代码会更简洁和更好地重用,因为如何执行一系列动作的机制是在(&lt;*&gt;) 中专门定义的。然而,应用序列在某种程度上也是机械的。所以当你想控制序列的流动或结果结构的“形状”时,你需要 monad 的额外力量,这会导致代码更冗长。

    我认为这个问题不是特别有帮助,如果人们投票支持关闭,我可以删除它。

    【讨论】:

      猜你喜欢
      • 2012-10-27
      • 2023-03-31
      • 1970-01-01
      • 1970-01-01
      • 2010-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多