Monad 通常轮流解释 return
和 bind
。但是,我想您也可以根据 join
(和 fmap
?)实现 bind
在缺乏一流函数的编程语言中,bind
使用起来非常尴尬。另一方面,join
看起来很简单。
但是,我不完全确定我了解 join
的工作原理。显然,它具有 [Haskell] 类型
join :: Monad m => m (m x) -> m x
对于列表 monad,这显然是微不足道的 concat
。但是对于一般的 monad,这种方法实际上在操作上做了什么?我看到了它对类型签名的作用,但我试图弄清楚如何用 Java 或类似语言编写类似的东西。
(实际上,这很容易:我不会。因为泛型被破坏了。;-) 但原则上问题仍然存在......)
哎呀。以前好像有人问过这个问题:
有人可以使用 return
、fmap
和 join
勾勒出一些常见 monad 的实现吗? (即,根本不提 >>=
。)我想也许这可能有助于它沉入我愚蠢的大脑......
join m = m >>= id
join
。至少,我知道产生正确类型签名的咒语。但我有点挣扎着想把我的想法包裹在字面上做什么......
concat
是一个容易发现的 join
候选者(考虑到一些基本的 Haskell 知识),而不是 the (即,只有一个)可能性。我提出我的问题是因为担心读者可能会从字面上理解你的评论并相信它是真实的,从而错过了一些有启发性的调查线索。例如,concat
是否满足所需的单子定律?其他候选人呢?如果是&不(分别),一个人如何导出 concat
来自法律?我用这样的问题来引导我远离“显而易见的”,即我的盲点。
在不深入隐喻的情况下,我是否建议将典型的 monad m
解读为“产生 a 的策略”,因此类型 m value
是一流的“产生值的策略”。计算或外部交互的不同概念需要不同类型的策略,但一般概念需要一些规则结构才能有意义:
如果你已经有了一个值,那么你有一个策略来产生一个值(return :: v -> mv),除了产生你所拥有的值之外什么都不包含;
如果您有一个将一种值转换为另一种值的函数,您可以将其提升为策略 (fmap :: (v -> u) -> mv -> mu),只需等待策略传递其值,然后转换它;
如果你有一个策略来产生一个策略来产生一个值,那么你可以构造一个策略来产生一个值 (join :: m (mv) -> mv),它遵循外部策略,直到它产生内部策略,然后遵循那种内在的策略一直到价值。
举个例子:叶子标记的二叉树......
data Tree v = Leaf v | Node (Tree v) (Tree v)
...表示通过抛硬币来生产东西的策略。如果策略是 Leaf v
,那就是您的 v
;如果策略是 Node h t
,你掷硬币并继续策略 h
如果硬币显示“正面”,t
如果它是“反面”。
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
的策略的策略。
https://i.stack.imgur.com/7ts7a.jpg
掷硬币:如果是“正面”,则掷另一枚硬币以在两种策略之间做出决定(分别为“掷硬币产生 0 或产生 1”或“产生 2”);如果是“反面”,则产生第三个(“掷硬币产生 3 或掷硬币产生 4 或 5”)。
显然,join
需要制定一个产生 Int
的策略。
https://i.stack.imgur.com/Dw5vO.jpg
我们正在利用的是这样一个事实,即“产生价值的策略”本身可以被视为一种价值。在 Haskell 中,将策略嵌入为值是沉默的,但在英语中,我使用引号来区分使用策略和仅仅谈论它。 join
运算符表示策略“以某种方式产生然后遵循策略”,或“如果您被告知一个策略,那么您可以使用它”。
(元。我不确定这种“策略”方法是否是一种适当的通用方式来考虑单子和值/计算的区别,或者它是否只是另一个糟糕的隐喻。我确实发现叶子标记的树状类型很有用直觉的来源,这也许并不奇怪,因为它们是自由的单子,具有足够的结构来成为单子,但仅此而已。)
PS“绑定”的类型
(>>=) :: m v -> (v -> m w) -> m w
说“如果你有一个产生 v
的策略,并且对于每个 va 后续策略产生一个 w
,那么你就有一个产生 w
的策略”。我们如何用 join
来捕捉它?
mv >>= v2mw = join (fmap v2mw mv)
我们可以通过 v2mw
重新标记我们的 v
-生成策略,而不是每个 v
值生成后续的 w
-生成策略 — 准备好 join
!
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
调用 fmap (f :: a -> m b) (x ::
m
a)
会产生值 (y ::
m
(m b))
,因此非常自然使用 join
取回值 (z :: m b)
。
然后将 bind 简单地定义为 bind ma f = join (fmap f ma)
,从而实现 (:: a -> m b)
多种函数的 Kleisly compositionality,这就是它的真正意义所在:
ma `bind` (f >=> g) = (ma `bind` f) `bind` g -- bind = (>>=)
= (`bind` g) . (`bind` f) $ ma
= join . fmap g . join . fmap f $ ma
因此,对于 flip bind = (=<<)
,we have
((g <=< f) =<<) = (g =<<) . (f =<<) = join . (g <$>) . join . (f <$>)
https://i.stack.imgur.com/G5uCl.png
(return . f' =<<) = (f' <$>) = join . (return . f' <$>)
。 (g' <$>) . (f' <$>) = (return . g' =<<) . (return . f' =<<) = (return . g') <=< (return . f')
。 (注意 f' 和 g' 这里是“水平的”,而不是“对角线的”。)
好的,所以回答你自己的问题并不是一个很好的形式,但我会记下我的想法,以防它启发其他人。 (我对此表示怀疑...)
如果可以将 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)
那是……无痛的。 Reader
实际上只是一个接受全局状态然后才返回其结果的函数。因此,要取消堆栈,您将全局状态应用于外部操作,该操作返回一个新的 Reader
。然后,您也将状态应用于此内部函数。
在某种程度上,它可能比通常的方式更容易:
Reader f >>= g = Reader (\ s -> let x = f s in g x)
现在,哪个是 reader 函数,哪个是计算下一个 reader 的函数......?
现在让我们试试旧的 State
monad。这里每个函数都将初始状态作为输入,但也会返回一个新状态及其输出。
data State s x = State (s -> (s, x))
join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
那不是太难。它基本上是运行然后运行。
我现在要停止打字了。随时指出我的示例中的所有故障和错别字... :-/
join (Just mx)
中缺少括号。此外,回答您自己的问题也不错,参见。 blog.stackoverflow.com/2011/07/…
我发现许多关于 monad 的解释说“你不必了解范畴论,真的,只要把 monad 想象成墨西哥卷饼 / 太空服 / 什么的”。
真的,那篇为我揭开 monad 神秘面纱的文章只是说了什么是类别,用类别来描述 monad(包括 join 和 bind),并没有理会任何虚假的隐喻:
http://en.wikibooks.org/wiki/Haskell/Category_theory
我认为这篇文章非常易读,不需要太多数学知识。
询问 Haskell 中的类型签名做什么,就像询问 Java 中的接口做什么。
从字面上看,它“没有”。 (当然,你通常会有某种与之相关的目的,这主要是在你的脑海中,而不是在实现中。)
在这两种情况下,您都在声明将在以后的定义中使用的语言中的合法符号序列。
当然,在 Java 中,我想您可以说接口对应于类型签名,该类型签名将在 VM 中逐字实现。您可以通过这种方式获得一些多态性——您可以定义一个接受接口的名称,并且您可以为接受不同接口的名称提供不同的定义。 Haskell 中也发生了类似的事情,您可以为接受一种类型的名称提供一个声明,然后为该名称提供另一个处理不同类型的声明。
这是 Monad 在一张图片中解释的。绿色类别中的2个函数是不可组合的,当用join . fmap
映射到蓝色类别时(严格来说,它们是一个类别),它们变成了可组合的。 Monad 是关于将 T -> Monad<U>
类型的函数转换为 Monad<T> -> Monad<U>
类型的函数。
https://i.stack.imgur.com/wOlfQ.png
concat
将事物选择之间的选择变成事物的选择,就像将一条布满商店的街道(先选择商店,然后选择东西)变成一个大超市。v2mw :: v -> m w
和fmap :: (a -> b) -> m a -> m b
所以fmap v2mw
类型检查a = v
和b = m w
给类型m v -> m (m w)
,这就是为什么join
之后给你m w
。