首页 文章

Monad对申请人有什么好处?

提问于
浏览
33

我读过this article,但最后一节并不明白 .

作者说Monad给了我们上下文敏感性,但是只使用Applicative实例就可以实现相同的结果:

let maybeAge = (\futureYear birthYear -> if futureYear < birthYear
    then yearDiff birthYear futureYear
    else yearDiff futureYear birthYear) <$> (readMay futureYearString) <*> (readMay birthYearString)

没有do语法肯定会更加丑陋,但除此之外我不明白为什么我们需要Monad . 任何人都可以为我清除这个吗?

7 回答

  • 24

    这里有几个使用 Monad 接口的函数 .

    ifM :: Monad m => m Bool -> m a -> m a -> m a
    ifM c x y = c >>= \z -> if z then x else y
    
    whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
    whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)
    

    您无法使用 Applicative 接口实现它们 . 但是为了启蒙,我们试着看看哪里出了问题 . 怎么样..

    import Control.Applicative
    
    ifA :: Applicative f => f Bool -> f a -> f a -> f a
    ifA c x y = (\c' x' y' -> if c' then x' else y') <$> c <*> x <*> y
    

    看起来不错!它有正确的类型,它必须是同一个东西!我们来检查以确保..

    *Main> ifM (Just True) (Just 1) (Just 2)
    Just 1
    *Main> ifM (Just True) (Just 1) (Nothing)
    Just 1
    *Main> ifA (Just True) (Just 1) (Just 2)
    Just 1
    *Main> ifA (Just True) (Just 1) (Nothing)
    Nothing
    

    并且's your first hint at the difference. You can'只使用复制 ifMApplicative 接口编写函数 .

    如果你把它分解为将 f a 形式的值视为"effects"和"results"(两者都是非常模糊的近似术语,这是最好的术语,但不是很好),你可以在这里提高你的理解 . 对于 Maybe a 类型的值,"effect"是成功或失败,作为计算 . "result"是 a 类型的值,在计算完成时可能存在 . (这些术语的含义在很大程度上取决于具体类型,所以不要认为这是除了 Maybe 之外的任何其他类型的有效描述 . )

    鉴于这种设置,我们可以更深入地看待差异 . Applicative 接口允许"result"控制流是动态的,但它需要"effect"控制流是静态的 . 如果您的表达式涉及3个可能失败的计算,则其中任何一个的失败都会导致整个计算失败 . Monad 界面更灵活 . 它允许"effect"控制流依赖于"result"值 . ifM 根据第一个参数选择哪个参数的"effects"包含在自己的"effects"中 . 这是 ifAifM 之间的巨大根本区别 .

    whileM 正在发生更严重的事情 . 让我们尝试制作 whileA ,看看会发生什么 .

    whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
    whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)
    

    嗯..发生了什么是编译错误 . (<*>) 那里没有合适的类型 . whileA p step 的类型为 a -> f astep x 的类型为 f a . (<*>) 不是合适的形状 . 要使其工作,函数类型必须是 f (a -> a) .

    你可以尝试更多的东西 - 但你最终会发现 whileA 没有任何实现,甚至接近 whileM 的方式 . 我的意思是,你可以实现类型,但是没有办法使它既循环又终止 .

    使其工作需要 join(>>=) . (好吧,或其中之一的其中一个)和那些你从 Monad 界面得到的额外的东西 .

  • 21

    使用monad,后续效果可能取决于之前的值 . 例如,您可以:

    main = do
        b <- readLn :: IO Bool
        if b
          then fireMissiles
          else return ()
    

    你不能用 Applicative 来做 - 一个有效计算的结果值不能确定将遵循什么效果 .

    有点相关:

  • 7

    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!
    

    我们输入了错误的密码,但我们仍然得到了秘密!入侵者警报!这是因为 <$><*> ,或者相当于 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
    

    应该清楚为什么这有错误的行为 . 实际上,每一个applicative functor的使用都等同于表单的monadic代码

    do val1 <- app1
       val2 <- app2
       ...
       valN <- appN
       pure $ f val1 val2 ... valN
    

    (其中一些 appI 被允许为 pure xI 形式) . 并且等效地,该形式的任何monadic代码都可以被重写为

    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 形式的链,并且可能嵌套这些链(例如, f <$> (g <$> x <*> y) <*> z ) . 但是, (=<<) (或 (>>=) )允许我们取一个值并根据该值生成不同的monadic计算,这些计算可以在运行中构建 . 这就是我们用来决定是计算"print out the secret",还是计算"print out an intruder alert"的原因,以及为什么我们不能单独用applicative functor做出决定呢?应用功能的任何类型都不允许您使用纯粹的 Value .

    你可以用类似的方式与_653991一起考虑 joinas 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)
    

    当我们想要根据值选择不同的计算时会发生这种情况,但只有我们可以使用的应用功能 . 我们可以选择两种不同的计算来返回,但是他们选择了're wrapped inside the outer layer of the applicative functor. To actually use the computation we',我们需要 join

    checkPasswordM' :: IO ()
    checkPasswordM' = join checkPasswordA'
    

    这与之前的monadic版本完全相同(只要我们 import Control.Monad 首先获得 join ):

    *Main> checkPasswordM'
    What's the password?
    12345
    INTRUDER ALERT!  INTRUDER ALERT!
    
  • 5

    另一方面,这是一个 Applicative / Monad 划分的实际例子,其中 Applicative 具有优势:错误处理!我们显然有 MonadMonad 实现,它带有错误,但它总是提前终止 .

    Left e1 >> Left e2    ===   Left e1
    

    您可以将此视为混合值和上下文的效果 . 由于 (>>=) 将尝试将 Either e a 值的结果传递给类似 a -> Either e b 的函数,因此如果输入 EitherLeft ,它必须立即失败 .

    Applicative 仅在运行所有效果后将其值传递给最终的纯计算 . 这意味着他们可以延迟访问值更长时间,我们可以写这个 .

    data AllErrors e a = Error e | Pure a deriving (Functor)
    
    instance Monoid e => Applicative (AllErrors e) where
      pure = Pure
      (Pure f) <*> (Pure x) = Pure (f x)
      (Error e) <*> (Pure _) = Error e
      (Pure _) <*> (Error e) = Error e
      -- This is the non-Monadic case
      (Error e1) <*> (Error e2) = Error (e1 <> e2)
    

    AllErrors 编写 Monad 实例是不可能的,因为 ap 匹配 (<*>) ,因为 (<*>) 在使用任何值之前利用运行第一个和第二个上下文来获取两个错误并将它们组合在一起 . Monad ic (>>=)(join) 只能访问与其值交织在一起的上下文 . 这就是为什么 EitherApplicative 实例是左偏的,所以它也可以有一个和谐的 Monad 实例 .

    > Left "a" <*> Left "b"
    Left 'a'
    
    > Error "a" <*> Error "b"
    Error "ab"
    
  • 55

    使用Applicative,要执行的有效动作序列在编译时固定 . 使用Monad,它可以根据效果的结果在运行时变化 .

    例如,使用Applicative解析器,解析操作的序列始终是固定的 . 这意味着您可以对其执行“优化” . 另一方面,我可以编写一个Monadic解析器来解析一些BNF语法描述,动态构造该语法的解析器,然后在其余输入上运行该解析器 . 每次运行此解析器时,它都可能构造一个全新的解析器来解析输入的第二部分 . Applicative没有希望做这样的事情 - 并且没有机会对尚不存在的解析器执行编译时优化...

    正如您所看到的,有时候Applicative的“限制”实际上是有益的 - 有时Monad提供的额外功能是完成工作所必需的 . 这就是我们两者兼得的原因 .

  • 10

    如果您尝试将Monad的 bind 和Applicative <*> 的类型签名转换为自然语言,您会发现:

    bindI 将为您提供所包含的值, you 将返回一个新的打包值

    <*>You 给我一个打包的函数,它接受一个包含的值并返回一个值, I 将根据我的规则使用它来创建新的打包值 .

    现在您可以从上面的描述中看到,与 <*> 相比, bind 为您提供了更多控制权

  • 5

    如果您使用Applicatives,结果的"shape"已由输入的"shape"确定,例如如果你调用 [f,g,h] <*> [a,b,c,d,e] ,你的结果将是一个包含15个元素的列表,无论变量具有哪些值 . 你没有monad的这种保证/限制 . 考虑 [x,y,z] >>= join replicate :对于 [0,0,0] ,您将获得结果 [] ,对于 [1,2,3] ,结果为 [1,2,2,3,3,3] .

相关问题