monad 的表现力是以代码重用为代价的吗?

Does the expressiveness of monads come at the expense of code reuse?

当我比较Applicative和Monad类型的二元操作时class

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

两个区别变得明显:

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

我的结论可能有些天真甚至错误,因为我对 Haskell 和单子计算的经验很少。感谢黑暗中的任何光线!

如果你有纯函数 g :: a -> b 你可以通过

将它变成 Applicative 版本

pure g :: Applicative f => f (a -> b)

Monad 接近

pure . g :: Applicative f => a -> f b

所以您不会失去任何代码重用。

代码重用只有在您可以重用代码来完成您真正想要的事情时才有优势。

GHCi> putStrLn =<< getLine
Sulfur
Sulfur
GHCi> 

在这里,(=<<) 选择 getLineIO 上下文中产生的 String 结果并将其提供给 putStrLn,然后打印所述结果.

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

现在,在类型 fmap/(<$>)...

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 的额外功能,方法是将 (<$>) 替换为 (=<<),或者等效地在 IO (IO ()) 值上使用 join

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

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

这个问题的原因是我对functor、applicative和monad之间的关系理解过于笼统class。我以为只是重用纯函数。

(<*>) 本质上是说给我一个函数应用和一堆值应用,我会根据我的规则应用它们。

(=<<) 本质上说给我一个函数 (a -> m b) 和一个 monad,我将用 monad 的值提供给 (a -> m b) 并将其留给 (a -> m b) 来生成和 return 包装在同一个 monad 中的转换值。

当然,应用代码会更简洁,可重用性更好,因为如何执行一系列操作的机制是在 (<*>) 中专门定义的。然而,应用序列在某种程度上也是机械的。所以当你想控制序列的流动或结果结构的 "shape" 时,你需要 monad 的额外功能,这会导致更冗长的代码。

我认为这个问题不是特别有用,如果人们投票赞成关闭,我将毫无问题地删除它。