70

Monad 通常由return和轮流解释bind。但是,我认为您也可以bind根据join(和fmap?)

在缺乏一流功能的编程语言中,bind使用起来非常尴尬。join,另一方面,看起来很容易。

但是,我不完全确定我是否了解其join工作原理。显然,它具有 [Haskell] 类型

加入 :: Monad m => m (mx) -> mx

对于 list monad,这是微不足道的concat。但是对于一般的 monad,这种方法实际上在操作上做了什么?我看到了它对类型签名的作用,但我试图弄清楚如何用 Java 或类似语言编写类似的东西。

(实际上,这很容易:我不会。因为泛型被破坏了。;-) 但原则上问题仍然存在......)


哎呀。以前好像有人问过这个问题:

Monad 连接函数

return有人可以用,fmap和勾勒出一些常见单子的实现join吗?(即,根本不提>>=。)我想也许这可能有助于它沉入我愚蠢的大脑......

4

7 回答 7

102

在不深入隐喻的情况下,我是否建议将典型的 monad 解读m为“产生 a 的策略”,因此类型m value是一流的“产生价值的策略”。计算或外部交互的不同概念需要不同类型的策略,但一般概念需要一些规则结构才能有意义:

  • 如果你已经有了一个值,那么你就有了一个策略来产生一个值return :: v -> m v
  • 如果您有一个将一种价值转换为另一种价值的功能,您可以将其提升到策略(fmap :: (v -> u) -> m v -> m u),只需等待策略交付其价值,然后对其进行转换;
  • 如果你有一个策略来产生一个产生价值的策略,那么你可以构造一个策略来产生一个价值(join :: m (m v) -> m v),它遵循外部策略直到它产生内部策略,然后一直遵循那个内部策略直到一个价值。

举个例子:叶子标记的二叉树......

data Tree v = Leaf v | Node (Tree v) (Tree v)

...表示通过抛硬币来生产东西的策略。如果策略是Leaf v,那就是你的v;如果策略是,如果硬币显示“正面”,则Node h t您掷硬币并继续策略,如果它是“反面”。ht

instance Monad Tree where
  return = Leaf

策略生成策略是一棵带有树标签叶子的树:代替每个这样的叶子,我们可以嫁接给它贴标签的树......

  join (Leaf tree) = tree
  join (Node h t)  = Node (join h) (join t)

...当然我们有fmap它只是重新标记叶子。

instance Functor Tree where
  fmap f (Leaf x)    = Leaf (f x)
  fmap f (Node h t)  = Node (fmap f h) (fmap f t)

这是一个策略来产生一个策略来产生一个Int

树的树

掷硬币:如果是“正面”,则掷另一枚硬币以在两种策略之间做出决定(分别为“掷硬币产生 0 或产生 1”或“产生 2”);如果是“反面”,则产生第三个(“掷硬币产生 3 或掷硬币产生 4 或 5”)。

这显然join是为了制定一个产生Int.

在此处输入图像描述

我们正在利用的是这样一个事实,即“产生价值的策略”本身可以被视为一种价值。在 Haskell 中,将策略嵌入为值是沉默的,但在英语中,我使用引号来区分使用策略和仅仅谈论它。操作员表示策略“join以某种方式产生然后遵循策略”,或者“如果你被告知一个策略,你就可以使用它”。

(元。我不确定这种“策略”方法是否是一种适当的通用方式来考虑单子和值/计算的区别,或者它是否只是另一个糟糕的隐喻。我确实发现叶子标记的树状类型很有用直觉的来源,这也许并不奇怪,因为它们是自由的单子,具有足够的结构来成为单子,但仅此而已。)

PS“绑定”的类型

(>>=) :: m v -> (v -> m w) -> m w

说“如果你有一个策略来产生 a v,并且对于每个 va 后续策略来产生 a w,那么你就有一个策略来产生 a w”。我们如何才能捕捉到join呢?

mv >>= v2mw = join (fmap v2mw mv)

我们可以重新标记我们的v生产策略,通过v2mw生产而不是每个v值来生成后续的w生产策略——准备好了join

于 2012-06-28T09:46:51.680 回答
31
join = concat -- []
join f = \x -> f x x -- (e ->)
join f = \s -> let (f', s') = f s in f' s' -- State
join (Just (Just a)) = Just a; join _ = Nothing -- Maybe
join (Identity (Identity a)) = Identity a -- Identity
join (Right (Right a)) = Right a; join (Right (Left e)) = Left e; 
                                  join (Left e) = Left e -- Either
join ((a, m), m') = (a, m' `mappend` m) -- Writer
-- N.B. there is a non-newtype-wrapped Monad instance for tuples that
-- behaves like the Writer instance, but with the tuple order swapped
join f = \k -> f (\f' -> f' k) -- Cont
于 2012-06-27T21:42:27.267 回答
19

调用会产生值,因此使用它来取回值是一件很自然的事情fmap (f :: a -> m b) (x ::ma)(y ::m(m b))join(z :: m b)

然后 bind 被简单地定义为bind ma f = join (fmap f ma),从而实现了多样性函数的Kleisly 组合(:: a -> m b)性,这就是它的真正意义所在:

ma `bind` (f >=> g) = (ma `bind` f) `bind` g              -- bind = (>>=)
                    = (`bind` g) . (`bind` f) $ ma 
                    = join . fmap g . join . fmap f $ ma

所以,有了flip bind = (=<<)我们有

    ((g <=< f) =<<)  =  (g =<<) . (f =<<)  =  join . (g <$>) . join . (f <$>)

克莱斯利组成

于 2012-06-28T17:15:25.320 回答
17

好的,所以回答你自己的问题并不是一个很好的形式,但我会记下我的想法,以防它启发其他人。(我对此表示怀疑...)

如果一个 monad 可以被认为是一个“容器”,那么两者return都有join非常明显的语义。return生成一个单元素容器,并将join容器的容器变成单个容器。这没什么难的。

所以让我们专注于更自然地被认为是“动作”的单子。在这种情况下,m x是某种动作,x当你“执行”它时会产生一个类型的值。return x没有什么特别的,然后产生x. fmap f接受一个产生 的动作x,并构造一个计算x然后应用f到它的动作,并返回结果。到目前为止,一切都很好。

很明显,如果f它自己产生一个动作,那么你最终得到的是m (m x). 也就是说,一个计算另一个动作的动作。>>=在某种程度上,这可能比采取行动的函数和“产生行动的函数”等更容易让你思考。

所以,从逻辑上讲,它似乎join会运行第一个动作,执行它产生的动作,然后运行它。(或者更确切地说,join如果你想分裂头发,会返回一个执行我刚才描述的操作。)

这似乎是中心思想。要实现join,您要运行一个动作,然后它会为您提供另一个动作,然后您运行它。(无论“运行”对这个特定的 monad 意味着什么。)

鉴于这种见解,我可以尝试编写一些join实现:

join Nothing = Nothing
join (Just mx) = mx

如果外部动作是Nothing,则返回Nothing,否则返回内部动作。再说一次,Maybe它更像是一个容器而不是一个动作,所以让我们试试别的东西......

newtype Reader s x = Reader (s -> x)

join (Reader f) = Reader (\ s -> let Reader g = f s in g s)

那是……无痛的。AReader实际上只是一个接受全局状态的函数,然后才返回其结果。因此,要取消堆栈,您将全局状态应用于外部操作,该操作返回一个新的Reader. 然后,您也将状态应用于此内部函数。

在某种程度上,它可能比通常的方式更容易:

Reader f >>= g = Reader (\ s -> let x = f s in g x)

现在,哪个是 reader 函数,哪个是计算下一个 reader 的函数......?

现在让我们试试旧的State单子。这里每个函数都将初始状态作为输入,但也会返回一个新状态及其输出。

data State s x = State (s -> (s, x))

join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)

那不是太难。它基本上是运行然后运行。

我现在要停止打字了。随时指出我的示例中的所有故障和错别字... :-/

于 2012-06-28T20:30:59.877 回答
13

我发现许多关于 monad 的解释说“你不必了解范畴论,真的,只要把 monad 想象成墨西哥卷饼 / 太空服 / 什么的”。

真的,那篇为我揭开 monad 神秘面纱的文章只是说了什么是类别,用类别来描述 monad(包括 join 和 bind),并没有理会任何虚假的隐喻:

我认为这篇文章非常易读,不需要太多数学知识。

于 2012-06-28T05:04:00.467 回答
4

询问 Haskell 中的类型签名什么就像询问 Java 中的接口做什么

从字面上看,它“没有”。(当然,你通常会有某种与之相关的目的,这主要是在你的脑海中,而不是在实现中。)

在这两种情况下,您都在声明将在以后的定义中使用的语言中的合法符号序列。

当然,在 Java 中,我想您可以说接口对应于类型签名,该类型签名将在 VM 中逐字实现。您可以通过这种方式获得一些多态性——您可以定义一个接受接口的名称,并且您可以为接受不同接口的名称提供不同的定义。Haskell 中也发生了类似的事情,您可以为接受一种类型的名称提供一个声明,然后为该名称提供另一个处理不同类型的声明。

于 2012-06-28T15:11:59.213 回答
2

这是 Monad 在一张图片中解释的。绿色类别中的2个函数是不可组合的,当映射到蓝色类别(严格来说,它们是一个类别)时,它们变成了可组合的。Monad 是关于将 typeT -> Monad<U>的函数转换为 的函数Monad<T> -> Monad<U>

Monad 在一张图片中解释了这一点。

于 2017-05-08T04:29:42.247 回答