Haskell中单子与应用的区别

kmynzznz  于 2022-12-23  发布在  其他
关注(0)|答案(7)|浏览(190)

我刚刚从typeclassopedia上读到了MonadApplicative之间的区别。我可以理解Applicative中没有join。但是下面的描述在我看来很模糊,我无法弄清楚一元计算/操作的“结果”到底是什么意思。因此,如果我把一个值放入Maybe中,它就产生了一元。这种“计算”的结果是什么?
我们再仔细看看(〉〉=)的类型,基本的直觉是它把两个计算合并成一个更大的计算,第一个参数m a是第一个计算,但是如果第二个参数只是一个m b,那就太无聊了;那么计算就无法相互交互(实际上,这正是应用型的情况),因此,(〉〉=)的第二个参数的类型为a -〉m b:给定第一次计算的结果,这种类型的函数可以产生要运行的第二次计算。...直观地说,正是这种使用先前计算的输出来决定接下来运行什么计算的能力使得Monad比应用性更强大。应用性计算的结构是固定的,而Monad计算的结构可以基于中间结果而改变。
有没有一个具体的例子来说明“使用前面计算的输出来决定下一次运行什么计算的能力”,而应用程序没有?

yx2lnoni

yx2lnoni1#

我最喜欢的例子是“纯粹应用的Either”。我们将从分析Either的基本Monad示例开始

instance Monad (Either e) where
  return = Right
  Left e  >>= _ = Left e
  Right a >>= f = f a

这个例子嵌入了一个非常自然的短路概念:我们从左到右进行,一旦一个计算“失败”到Left中,那么所有其他的计算也会失败。

instance Applicative (Either e) where
  pure  = return
  (<*>) = ap

其中ap只不过是return之前的从左到右的排序:

ap :: Monad m => m (a -> b) -> m a -> m b
ap mf ma = do 
  f <- mf
  a <- ma
  return (f a)

现在,当你想收集计算中任何地方出现的错误消息并以某种方式生成错误摘要时,这个Either示例的问题就暴露出来了,这与短路背道而驰,也与(>>=)类型背道而驰

(>>=) :: m a -> (a -> m b) -> m b

如果我们把m a看作“过去”,把m b看作“未来”,那么(>>=)只要能运行“步进器”(a -> m b),就能从过去产生未来。这个“步进器”要求a的价值确实存在于未来......而这对Either来说是不可能的。因此(>>=) * 要求 * 短-电路。
因此,我们将实现一个Applicative示例,该示例不能有对应的Monad

instance Monoid e => Applicative (Either e) where
  pure = Right

现在(<*>)的实现是值得仔细考虑的特殊部分,它在前3个例子中执行了一些“短路”,但在第4个例子中做了一些有趣的事情。

Right f <*> Right a = Right (f a)     -- neutral
  Left  e <*> Right _ = Left e          -- short-circuit
  Right _ <*> Left  e = Left e          -- short-circuit
  Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!

再次注意,如果我们把左边的参数看作“过去”,右边的参数看作“未来”,那么(<*>)(>>=)相比是特殊的,因为它可以并行地“打开”未来和过去,而不必为了计算“未来”而需要“过去”的结果。
这直接意味着,我们可以使用纯ApplicativeEither来收集错误,如果链中存在任何Left,则忽略Right

> Right (+1) <*> Left [1] <*> Left [2]
> Left [1,2]

所以让我们把这个直觉颠倒过来。对于一个纯粹的应用型Either,我们有什么不能做的呢?好吧,因为它的操作依赖于在运行过去之前检查未来,所以我们必须能够确定未来的结构,而不依赖于过去的值。换句话说,我们不能写

ifA :: Applicative f => f Bool -> f a -> f a -> f a

其满足以下等式

ifA (pure True)  t e == t
ifA (pure False) t e == e

我们可以写ifM

ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM mbool th el = do
  bool <- mbool
  if bool then th else el

使得

ifM (return True)  t e == t
ifM (return False) t e == e

这是不可能的,因为ifA完全体现了结果计算的思想,它取决于参数计算中嵌入的值。

qybjjes1

qybjjes12#

Just 1描述“计算”,其“结果”是1。Nothing描述不产生结果的计算。
单子和应用式的区别在于单子中有一个选择。单子的关键区别在于在计算中选择不同路径的能力(不仅仅是早期的突破)。根据计算中前一步产生的值,计算结构的其余部分可以改变。
这就是它的意思在一元链中

return 42            >>= (\x ->
if x == 1
   then
        return (x+1) 
   else 
        return (x-1) >>= (\y -> 
        return (1/y)     ))

if选择要构造什么计算。
如果适用,在

pure (1/) <*> ( pure (+(-1)) <*> pure 1 )

所有的函数都在“内部”计算中工作,没有机会打破一个链。每个函数只是转换它所输入的值。从函数的Angular 来看,计算结构的“形状”完全是“外部”的。
一个函数可以返回一个特殊的值来表示失败,但它不能导致计算中的后续步骤被跳过。它们都必须以特殊的方式处理这个特殊的值。计算的形式不能根据接收到的值来改变。
对于单子,函数本身根据自己的选择构造计算。

yftpprvb

yftpprvb3#

下面是我对@J. Abrahamson的例子的看法,为什么ifA不能使用(pure True)中的值,本质上,它仍然归结为ApplicativeMonad缺少join函数,这统一了类型类中给出的两个不同的视角来解释MonadApplicative之间的区别。
因此,使用@J. Abrahamson的纯粹应用型Either的例子:

instance Monoid e => Applicative (Either e) where
  pure = Right

  Right f <*> Right a = Right (f a)     -- neutral
  Left  e <*> Right _ = Left e          -- short-circuit
  Right _ <*> Left  e = Left e          -- short-circuit
  Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!

(其具有与EitherMonad类似的短路效果)和ifA函数

ifA :: Applicative f => f Bool -> f a -> f a -> f a

如果我们尝试实现上述方程:

ifA (pure True)  t e == t
ifA (pure False) t e == e


好吧,正如已经指出的,最终,(pure True)的内容不能被后面的计算使用,但从技术上讲,这是不对的,我们可以使用(pure True)的内容,因为Monad也是Functorfmap的组合,我们可以:

ifA' b t e = fmap (\x -> if x then t else e) b

问题出在ifA'的返回类型f (f a)上,在Applicative中,无法将两个嵌套的Applicative S折叠成一个,但这个折叠函数正是joinMonad中执行的,因此,

ifA = join . ifA'

将满足ifA的方程,如果我们能够适当地实现join。这里缺少的正是Applicative函数。换句话说,我们可以使用Applicative中先前结果的结果。但是在Applicative框架中这样做将涉及将返回值的类型扩充为嵌套的应用值,我们无法将其恢复为单层应用值。这将是一个严重的问题,因为例如我们无法适当地使用Applicative S来组合函数。使用join解决了该问题,但是正是X1 M30 N1 X的引入将X1 M31 N1 X提升为X1 M32 N1 X。

k5ifujac

k5ifujac4#

差异的关键可以在ap的类型与=<<的类型中观察到。

ap :: m (a -> b) -> (m a -> m b)
=<< :: (a -> m b) -> (m a -> m b)

在两种情况下都有m a,但只有在第二种情况下m a才能决定是否应用函数(a -> m b)。反过来,函数(a -> m b)可以“决定”是否应用下一个函数界限--通过产生不“包含”bm b(例如[]NothingLeft)。
Applicative中,m (a -> b)“内部”的函数无法做出这样的“决定”--它们总是生成b类型的值。

f 1 = Nothing -- here f "decides" to produce Nothing
f x = Just x

Just 1 >>= f >>= g -- g doesn't get applied, because f decided so.

Applicative中这是不可能的,所以不能给出例子。

f 1 = 0
f x = x

g <$> f <$> Just 1 -- oh well, this will produce Just 0, but can't stop g
                   -- from getting applied
qq24tv8q

qq24tv8q5#

但是下面的描述在我看来很模糊,我无法弄清楚一元计算/操作的“结果”到底是什么意思。
当然,这种模糊是故意的,因为一元计算的“结果”取决于每种类型,最好的回答有点同义反复:“result”(或 results,因为可能不止一个)是(>>=) :: Monad m => m a -> (a -> m b) -> m b示例的实现调用函数参数时使用的任何值。
那么,如果我把一个值放入Maybe中,它会生成一个单子,那么这个“计算”的结果是什么呢?
Maybe单子如下所示:

instance Monad Maybe where
    return = Just
    Nothing >>= _ = Nothing
    Just a >>= k = k a

这里唯一有资格作为“结果”的是>>=的第二个等式中的a,因为它是唯一被“馈入”>>=的第二个参数的东西。
关于ifAifM之间的差异,其他答案已经深入讨论过,因此我想强调另一个显著差异:应用合成,单子不合成。对于Monad s,如果你想创建一个Monad来组合两个现有的应用,你必须重写其中一个作为单子转换器。相反,如果你有两个Applicatives,你可以很容易地从它们中创建一个更复杂的应用,如下所示。(代码复制自transformers。)

-- | The composition of two functors.
newtype Compose f g a = Compose { getCompose :: f (g a) }

-- | The composition of two functors is also a functor.
instance (Functor f, Functor g) => Functor (Compose f g) where
    fmap f (Compose x) = Compose (fmap (fmap f) x)

-- | The composition of two applicatives is also an applicative.
instance (Applicative f, Applicative g) => Applicative (Compose f g) where
    pure x = Compose (pure (pure x))
    Compose f <*> Compose x = Compose ((<*>) <$> f <*> x)

-- | The product of two functors.
data Product f g a = Pair (f a) (g a)

-- | The product of two functors is also a functor.
instance (Functor f, Functor g) => Functor (Product f g) where
    fmap f (Pair x y) = Pair (fmap f x) (fmap f y)

-- | The product of two applicatives is also an applicative.
instance (Applicative f, Applicative g) => Applicative (Product f g) where
    pure x = Pair (pure x) (pure x)
    Pair f g <*> Pair x y = Pair (f <*> x) (g <*> y)

-- | The sum of a functor @f@ with the 'Identity' functor
data Lift f a = Pure a | Other (f a)

-- | The sum of two functors is always a functor.
instance (Functor f) => Functor (Lift f) where
    fmap f (Pure x) = Pure (f x)
    fmap f (Other y) = Other (fmap f y)

-- | The sum of any applicative with 'Identity' is also an applicative 
instance (Applicative f) => Applicative (Lift f) where
    pure = Pure
    Pure f <*> Pure x = Pure (f x)
    Pure f <*> Other y = Other (f <$> y)
    Other f <*> Pure x = Other (($ x) <$> f)
    Other f <*> Other y = Other (f <*> y)

现在,如果我们加入Constant函子/应用式:

newtype Constant a b = Constant { getConstant :: a }

instance Functor (Constant a) where
    fmap f (Constant x) = Constant x

instance (Monoid a) => Applicative (Constant a) where
    pure _ = Constant mempty
    Constant x <*> Constant y = Constant (x `mappend` y)

......我们可以从LiftConstant的其他响应中组合出“应用型Either“:

type Error e a = Lift (Constant e) a
bd1hkmkf

bd1hkmkf6#

正如@Will Ness在他的回答中所解释的,关键的区别在于Monads在每一步都有不同执行路径的选择,让我们通过实现一个四次排序的函数来使这个潜在的选择在语法上可见,首先是应用型f,然后是monad m

seq4A :: Applicative f => f a -> f [a]
seq4A f =
    f <**> (
    f <**> (
    f <**> (
    f <&> (\a1 a2 a3 a4 -> 
        [a1, a2, a3, a4]))))

seq4M :: Monad m => m a -> m [a]
seq4M m =
    m >>= (\a1 ->
    m >>= (\a2 ->
    m >>= (\a3 ->
    m >>= (\a4 -> 
        return [a1, a2, a3, a4]))))

seq4M函数在每一步都有一元操作的结果值,因此可以在每一步进行选择,而seq4A函数只有最后一步的结果值。

pgvzfuti

pgvzfuti7#

我想分享我对这个“iffy miffy”的看法,因为我理解这一切都在上下文得到应用,所以例如:

iffy :: Applicative f => f Bool -> f a -> f a -> f a
iffy fb ft fe = cond <$> fb <*> ft <*> fe   where
            cond b t e = if b then t else e

case 1>> iffy (Just True) (Just “True”) Nothing ->> Nothing

upps应该只是“真的”...但是

case 2>> iffy (Just False) (Just “True”) (Just "False") ->> Just "False"

(the“好”的选择是在上下文中做出的)我这样对自己解释,就在计算结束之前,在〉〉1的情况下,我们在“链”中得到类似的东西:

Just (Cond True "True") <*> something [something being "accidentaly" Nothing]

根据适用性的定义,其被评估为:

fmap (Cond True "True") something

当“something”时,根据函子约束(fmap over Nothing给出Nothing),Nothing变为Nothing。并且不可能定义具有“fmap f Nothing = something”的函子。

相关问题