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
两个区别变得明显:
ap
期望一个正常的纯函数,而 bind
期望一个 monadic 动作,它必须 return 相同类型的 monad
- 对于
ap
,动作顺序由应用程序决定,而对于bind
,单子动作可以确定控制流
所以单子给了我额外的表达能力。然而,由于他们不再接受普通的纯函数,这种表现力似乎是以代码重用为代价的。
我的结论可能有些天真甚至错误,因为我对 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>
在这里,(=<<)
选择 getLine
在 IO
上下文中产生的 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一样,我也很难理解你问题的前提。您似乎期望 Functor
、Applicative
和 Monad
中的一个会比其他的更好。事实并非如此。我们可以使用 Applicative
比使用 Functor
做更多的事情,使用 Monad
甚至更多。如果您需要额外的力量,请使用适当强大的 class。如果不是,使用功能较弱的 class 将导致更简单、更清晰的代码和更广泛的可用实例。
这个问题的原因是我对functor、applicative和monad之间的关系理解过于笼统class。我以为只是重用纯函数。
(<*>)
本质上是说给我一个函数应用和一堆值应用,我会根据我的规则应用它们。
(=<<)
本质上说给我一个函数 (a -> m b)
和一个 monad,我将用 monad 的值提供给 (a -> m b)
并将其留给 (a -> m b)
来生成和 return 包装在同一个 monad 中的转换值。
当然,应用代码会更简洁,可重用性更好,因为如何执行一系列操作的机制是在 (<*>)
中专门定义的。然而,应用序列在某种程度上也是机械的。所以当你想控制序列的流动或结果结构的 "shape" 时,你需要 monad 的额外功能,这会导致更冗长的代码。
我认为这个问题不是特别有用,如果人们投票赞成关闭,我将毫无问题地删除它。
当我比较Applicative和Monad类型的二元操作时class
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(=<<) :: Monad m => (a -> m b) -> m a -> m b
两个区别变得明显:
ap
期望一个正常的纯函数,而bind
期望一个 monadic 动作,它必须 return 相同类型的 monad- 对于
ap
,动作顺序由应用程序决定,而对于bind
,单子动作可以确定控制流
所以单子给了我额外的表达能力。然而,由于他们不再接受普通的纯函数,这种表现力似乎是以代码重用为代价的。
我的结论可能有些天真甚至错误,因为我对 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>
在这里,(=<<)
选择 getLine
在 IO
上下文中产生的 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一样,我也很难理解你问题的前提。您似乎期望 Functor
、Applicative
和 Monad
中的一个会比其他的更好。事实并非如此。我们可以使用 Applicative
比使用 Functor
做更多的事情,使用 Monad
甚至更多。如果您需要额外的力量,请使用适当强大的 class。如果不是,使用功能较弱的 class 将导致更简单、更清晰的代码和更广泛的可用实例。
这个问题的原因是我对functor、applicative和monad之间的关系理解过于笼统class。我以为只是重用纯函数。
(<*>)
本质上是说给我一个函数应用和一堆值应用,我会根据我的规则应用它们。
(=<<)
本质上说给我一个函数 (a -> m b)
和一个 monad,我将用 monad 的值提供给 (a -> m b)
并将其留给 (a -> m b)
来生成和 return 包装在同一个 monad 中的转换值。
当然,应用代码会更简洁,可重用性更好,因为如何执行一系列操作的机制是在 (<*>)
中专门定义的。然而,应用序列在某种程度上也是机械的。所以当你想控制序列的流动或结果结构的 "shape" 时,你需要 monad 的额外功能,这会导致更冗长的代码。
我认为这个问题不是特别有用,如果人们投票赞成关闭,我将毫无问题地删除它。