正如 Stephen Tetley 在评论中所说,该示例实际上并未使用上下文敏感性。考虑上下文敏感性的一种方法是,它允许使用根据单子值选择要采取的操作。在某种意义上,应用计算必须始终具有相同的“形状”,而不管所涉及的值如何;一元计算不需要。我个人认为这通过一个具体的例子更容易理解,所以让我们看一个。这是一个简单程序的两个版本,要求您输入密码,检查您输入的密码是否正确,然后根据您是否输入密码打印出响应。
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!
我们输入了错误的密码,但我们仍然得到了秘密! 还有入侵者警报!这是因为<$>
and<*>
或等效的/总是执行所有参数的效果。应用版本在符号上翻译为liftAn
liftMn
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
's 方法:
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
,并可能嵌套这些链(例如,f <$> (g <$> x <*> y) <*> z
)。但是,(=<<)
(或(>>=)
)允许我们取一个值并根据该值产生不同的一元计算,这些计算可以即时构建。这就是我们用来决定是计算“打印出秘密”还是计算“打印出入侵者警报”,以及为什么我们不能单独使用应用函子做出决定的原因;应用函数的任何类型都不允许您使用普通值。
你可以join
用fmap
类似的方式来考虑:正如我在评论中提到的,你可以做类似的事情
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!