As Stephen Tetley said in a comment,该示例实际上并未使用上下文相关性。考虑上下文敏感性的一种方法是,它允许使用根据单子值选择要采取的操作。在某种意义上,应用计算必须始终具有相同的“形状”,而不管所涉及的值如何;一元计算不需要。我个人认为这通过一个具体的例子更容易理解,所以让我们看一个。这是一个简单程序的两个版本,要求您输入密码,检查您输入的密码是否正确,然后根据您是否输入密码打印出响应。
import Control.Applicative
checkPasswordM :: IO ()
checkPasswordM = do putStrLn "What's the password?"
pass <- getLine
if pass == "swordfish"
then putStrLn "Correct. The secret answer is 42."
else putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
checkPasswordA :: IO ()
checkPasswordA = if' . (== "swordfish")
<$> (putStrLn "What's the password?" *> getLine)
<*> putStrLn "Correct. The secret answer is 42."
<*> putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
if' :: Bool -> a -> a -> a
if' True t _ = t
if' False _ f = f
让我们将它加载到 GHCi 并检查 monadic 版本会发生什么:
*Main> checkPasswordM
What's the password?
swordfish
Correct. The secret answer is 42.
*Main> checkPasswordM
What's the password?
zvbxrpl
INTRUDER ALERT! INTRUDER ALERT!
到目前为止,一切都很好。但是如果我们使用应用版本:
*Main> checkPasswordA
What's the password?
hunter2
Correct. The secret answer is 42.
INTRUDER ALERT! INTRUDER ALERT!
我们输入了错误的密码,但我们仍然得到了秘密! 并且一个入侵者警报!这是因为<$> 和<*>,或者等价的liftA<em>n</em>/liftM<em>n</em>,总是执行所有它们的参数的效果。应用程序版本以do 表示法转换为
do pass <- putStrLn "What's the password?" *> getLine)
unit1 <- putStrLn "Correct. The secret answer is 42."
unit2 <- putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
pure $ if' (pass == "swordfish") unit1 unit2
并且应该清楚为什么会有错误的行为。事实上,每一次应用函子的使用都等价于形式的一元代码
do val1 <- app1
val2 <- app2
...
valN <- appN
pure $ f val1 val2 ... valN
(其中一些appI 允许采用pure xI 的形式)。等效地,该形式的任何一元代码都可以重写为
f <$> app1 <*> app2 <*> ... <*> appN
或等价于
liftAN f app1 app2 ... appN
要考虑这一点,请考虑Applicative 的方法:
pure :: a -> f a
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
然后考虑Monad 添加的内容:
(=<<) :: (a -> m b) -> m a -> m b
join :: m (m a) -> m a
(请记住,您只需要其中一个。)
手忙脚乱,如果您考虑一下,我们可以将应用函数组合在一起的唯一方法是构造 f <$> app1 <*> ... <*> appN 形式的链,并可能嵌套这些链(例如,@ 987654342@)。但是,(=<<)(或(>>=))允许我们获取一个值并根据该值生成不同的单子计算,这些计算可以动态构建。这就是我们用来决定是计算“打印出秘密”还是计算“打印出入侵者警报”,以及为什么我们不能单独使用应用函子做出决定的原因;应用函数的任何类型都不允许您使用普通值。
你可以用类似的方式考虑join 和fmap:as I mentioned in a comment,你可以做类似的事情
checkPasswordFn :: String -> IO ()
checkPasswordFn pass = if pass == "swordfish"
then putStrLn "Correct. The secret answer is 42."
else putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
checkPasswordA' :: IO (IO ())
checkPasswordA' = checkPasswordFn <$> (putStrLn "What's the password?" *> getLine)
当我们想要根据值选择不同的计算时会发生这种情况,但我们只能使用应用程序功能。我们可以选择两个不同的计算来返回,但它们被包裹在应用函子的外层中。要实际使用我们选择的计算,我们需要join:
checkPasswordM' :: IO ()
checkPasswordM' = join checkPasswordA'
这和之前的monadic版本做的事情是一样的(只要我们先import Control.Monad,得到join):
*Main> checkPasswordM'
What's the password?
12345
INTRUDER ALERT! INTRUDER ALERT!