【问题标题】:Unlike a Functor, a Monad can change shape?与 Functor 不同,Monad 可以改变形状?
【发布时间】:2011-12-09 13:37:56
【问题描述】:

我一直很喜欢以下关于 monad 相对于 functor 的能力的直观解释:monad 可以改变形状;函子不能。

例如:length $ fmap f [1,2,3] 始终等于 3

但是,对于 monad,length $ [1,2,3] >>= g 通常不等于 3。例如,如果g 定义为:

g :: (Num a) => a -> [a]
g x = if x==2 then [] else [x]

那么[1,2,3] >>= g 等于[1,3]

让我有点困扰的是g 的类型签名。用通用的单子类型定义一个改变输入形状的函数似乎是不可能的,例如:

h :: (Monad m, Num a) => a -> m a

MonadPlus 或 MonadZero 类型类具有相关的零元素,可以用来代替 [],但现在我们拥有的不仅仅是 monad。

我说的对吗?如果是这样,有没有办法向 Haskell 的新手表达这种微妙之处。我想让我心爱的“单子可以改变形状”这句话,更诚实一点;如果需要的话。

【问题讨论】:

    标签: haskell monads


    【解决方案1】:

    我一直很喜欢以下关于 monad 相对于 functor 的能力的直观解释:monad 可以改变形状;仿函数不能。

    顺便说一句,您在这里遗漏了一些微妙之处。为了术语,我将Haskell意义上的Functor分为三个部分:由类型参数确定并由fmap操作的参数组件,State中的元组构造函数等不变的部分,以及“形状”以及其他任何内容,例如构造函数之间的选择(例如,NothingJust)或涉及其他类型参数的部分(例如,Reader 中的环境)。

    当然,单独的Functor 仅限于将函数映射到参数部分。

    Monad 可以根据参数部分的值创建新的“形状”,这不仅可以更改形状。复制列表中的每个元素或删除前五个元素会改变形状,但过滤列表需要检查元素。

    这实质上就是Applicative 在它们之间的拟合方式——它允许您独立组合两个Functors 的形状和参数值,而不会让后者影响前者。

    我说的对吗?如果是这样,有没有办法向 Haskell 的新手表达这种微妙之处。我想让我心爱的“单子可以改变形状”这句话,更诚实一点;如果需要的话。

    也许您在这里寻找的微妙之处在于您并没有真正“改变”任何东西。 Monad 中的任何内容都不会让您明确地弄乱形状。它允许您根据每个参数值创建新形状,并将这些新形状重新组合成新的复合形状。

    因此,您将始终受到创建形状的可用方法的限制。对于完全通用的Monad,您所拥有的只是return,根据定义,它会创建任何必要的形状,使得(>>= return) 是标识函数。 Monad 的定义告诉你,给定特定类型的函数,你可以做什么;它不为您提供这些功能。

    【讨论】:

    • 但是你可以做sequence之类的事情,对吧?我会说这绝对是在“改变”结构。
    • @Rotsor:不是这样。 sequence 只是以与 (>>=) 相同的方式组合现有结构,但通过将整个列表混合在一起来实现。它不能做任何不在你应用它的列表中的事情。
    • 嗯,是的,它以有限的方式改变了结构——通过组合现有的形状,但它满足了要求:它有一个通用类型并且确实改变了形状。我已经发布了另一个示例作为我自己的答案。
    【解决方案2】:

    Monad 的操作可以“改变”值的形状,以至于>>= 函数将“树”中的叶节点替换为从节点值派生的新子结构(例如“树”的适当一般概念 - 在列表的情况下,“树”是关联的)。

    在您的列表示例中,发生的情况是每个数字(叶子)都被新列表替换,当g 应用于该数字时产生的新列表。如果您知道要查找的内容,仍然可以看到原始列表的整体结构; g 的结果还是按顺序排列的,它们只是被拼凑在一起,所以除非你已经知道,否则你无法判断一个结束和下一个开始。

    一个更有启发性的观点可能是考虑fmapjoin而不是>>=。与return 一起,任何一种方式都给出了monad 的等效定义。不过,在fmap/join 视图中,这里发生的事情更加清晰。继续您的列表示例,第一个 gfmapped 在列表上产生 [[1],[],[3]]。那么这个列表就是joined,对于列表来说就是concat

    【讨论】:

    • 如果你很好奇,等价显示为:join = (>>= id)fmap f = (>>= return . f)f >>= x = join (fmap f x)
    • 是的,我强烈支持使用join 而不是(>>=) 来定义monad。
    • @mokus:很好的答案!但我很确定你的意思是x >>= f = join (fmap f x)
    【解决方案3】:

    仅仅因为 monad 模式包含一些允许形状更改的特定实例,并不意味着 每个 实例都可以进行形状更改。例如,Identity monad 中只有一个“形状”可用:

    newtype Identity a = Identity a
    instance Monad Identity where
        return = Identity
        Identity a >>= f = f a
    

    事实上,我并不清楚很多单子都有有意义的“形状”:例如,StateReaderWriterSTSTM 中的形状是什么意思,或IO monads?

    【讨论】:

    • 没错,理解,但我个人觉得这个形状的概念,以及“作为容器的单子”,非常有用;甚至可以帮助通知外行。一般细节可以稍后解决。
    • 这里的“形状”一词基本上是“状态”或“效果”的同义词——Functor 中既不是参数也不是常数的部分。这个想法很有意义,但使用的术语不太理想。
    • @user643722 听起来你已经成为monad burrito fallacy 的牺牲品。它可以帮助您理解 monad,这很好,但实际上可能会让其他人更难理解它们。
    【解决方案4】:

    monad 的组合键是(>>=)。知道它由两个单子值组成并读取其类型签名后,单子的威力就变得更加明显:

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

    未来的行动可以完全取决于第一个行动的结果,因为它是其结果的函数。不过,这种能力是有代价的:Haskell 中的函数是完全不透明的,因此如果不实际运行组合动作,您将无法获得任何有关组合动作的信息。顺便说一句,这就是箭头的用武之地。

    【讨论】:

    • 我看到一个棘手的问题,因为我们到处都有 [a] 类型,但是有 不同 类型,我的意思是:g 可以表示为 g :: (Num a, Num b) => a -> [b] 然后你有(>>= g) :: (Num a, Num b) => [a] -> [b]
    【解决方案5】:

    具有h 之类的签名的函数除了对其参数执行一些算术运算之外,确实不能做许多有趣的事情。所以,你有正确的直觉。

    但是,查看functions with similar signatures 的常用库可能会有所帮助。你会发现最通用的,如你所料,执行通用的单子操作,如returnliftM,或join。此外,当您使用liftMfmap 将普通函数提升为一元函数时,您通常会得到一个类似的通用签名,这对于将纯函数与一元代码集成非常方便。

    为了使用特定 monad 提供的结构,您不可避免地需要使用有关您所在的特定 monad 的一些知识,以便在该 monad 中构建新的有趣的计算。考虑状态单子(s -> (a, s))。如果不知道那个类型,我们就不能写get = \s -> (s, s),但是如果不能访问状态,那么在 monad 中就没有多大意义了。

    【讨论】:

      【解决方案6】:

      我能想象到的满足要求的最简单的函数类型是这样的:

      enigma :: Monad m => m () -> m ()
      

      可以通过以下方式之一实现它:

      enigma1 m = m -- not changing the shape
      
      enigma2 _ = return () -- changing the shape
      

      这是一个非常简单的更改——enigma2 只是丢弃了形状并用微不足道的形状替换它。另一种通用变化是将两个形状组合在一起:

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

      foo 的结果可以具有与ab 不同的形状。

      第三个明显的形状变化,需要单子的全部力量,是一个

      join :: Monad m => m (m a) -> m a
      join x = x >>= id
      

      join x 的形状通常与x 本身的形状不同。

      结合这些原始的形状变化,可以推导出不平凡的东西,例如sequencefoldM 等。

      【讨论】:

      • 我没看到。使用join,我们仅限于单子的单子。同样,您的 enigma 变体似乎期望由单元类型参数化的 monad - 并产生相同的结果。
      • user643722,你的意思是功能没有你喜欢的一般?它们对m 进行抽象,因此它们适用于任何单子。这还不够吗?
      • @Rotsor:你不是说foo :: Monad m => m a -> m a -> m a吗?
      【解决方案7】:

      h :: (Monad m, Num a) => a -> m a
      h 0 = fail "Failed."
      h a = return a
      

      满足您的需求?例如,

      > [0,1,2,3] >>= h
      [1,2,3]
      

      【讨论】:

      • 很有趣,但我会在 failmzero 一起上课:在单子的正式定义之外。
      • 是的,fail 不属于Monad
      【解决方案8】:

      这不是一个完整的答案,但对于您的问题,我有几件事要说,但实际上不适合发表评论。

      首先,MonadFunctor 是类型类;他们对类型进行分类。所以说“单子可以改变形状;函子不能”是很奇怪的。我相信您要谈论的是“一元值”或“一元动作”:类型为 m a 的值对于某些类型为 Monad m 的类型为 * -> * 和其他类型的类型为 * .我不完全确定如何称呼Functor f :: f a,我想我会称它为“函子中的值”,尽管这不是对IO String 的最佳描述(IO 是函子)。

      其次,请注意,所有 Monad 都必须是 Functors (fmap = liftM),所以我想说你观察到的区别在 fmap>>= 之间,甚至在 fg 之间,而不是在MonadFunctor 之间。

      【讨论】:

      • 我知道你提出的观点。我试图让问题的语言保持非正式,尽管我可能已经过分了。正如其他人所说,任何事物都不会改变形状。毕竟我们使用的是 Haskell :)
      猜你喜欢
      • 1970-01-01
      • 2012-01-29
      • 2011-11-05
      • 1970-01-01
      • 1970-01-01
      • 2012-01-17
      • 1970-01-01
      相关资源
      最近更新 更多