Monad通常以return
和bind
轮流解释。但是,我知道您还可以bind
(和join
?)来实施fmap
在缺乏一流功能的编程语言中,bind
使用起来非常难以捉摸。另一方面,join
看起来很容易。
但我并不完全确定我理解join
的工作方式。显然,它有[Haskell]类型
join :: Monad m => m (m x) -> m x
对于列表monad,这很简单,显然是concat
。但是对于一般的monad来说,这种方法在操作上实际上做了什么?我看到它对类型签名的作用,但是我试图弄清楚我是如何在Java或类似的东西中写出这样的东西。
(实际上,这很容易:我不会。因为仿制品已经坏了。;-)但原则上问题仍然存在......)
糟糕。看起来之前有人问过:
有人可以使用return
,fmap
和join
草拟一些常见monad的实现吗? (即,根本没有提到>>=
。)我想也许这可能有助于它沉入我愚蠢的大脑......
答案 0 :(得分:92)
如果没有管道隐喻的深度,我可以建议将典型的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
,如果硬币显示“头部”,则投掷硬币然后按策略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
的策略。
抛掷一枚硬币:如果它是“头”,则抛出另一枚硬币来决定两种策略(分别产生“抛硬币生产0或生产1”或“生产2”);如果它是“尾巴”产生第三个(“投掷硬币生产3或掷硬币4或5”)。
显然join
制定了一个策略Int
。
我们正在利用的是“产生价值的策略”本身可以被视为一种价值。在Haskell中,策略作为值的嵌入是沉默的,但在英语中,我使用引号来区分使用策略而不仅仅是谈论它。 join
运算符表示策略“以某种方式生成然后遵循策略”,或“如果您告诉策略,则可以使用”。< / p>
(Meta。我不确定这种“策略”方法是否是一种适当的通用方式来考虑monad和价值/计算的区别,或者它是否只是另一个糟糕的比喻。我确实发现了叶子标记的树状键入一个有用的直觉来源,这可能不是一个惊喜,因为它们是 free monad,只有足够的结构可以成为monad,但不会更多。)
PS“绑定”的类型
(>>=) :: m v -> (v -> m w) -> m w
说“如果您有制定v
的策略,并且针对每个va后续策略生成w
,那么您就有策略来生成w
” 。我们如何根据join
?
mv >>= v2mw = join (fmap v2mw mv)
我们可以通过v
重新标记我们的v2mw
- 生成策略,而不是每个v
生成w
- 生成策略,从而生成{准备{ {1}}!
答案 1 :(得分:24)
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
join f = \k -> f (\f' -> f' k) -- Cont
答案 2 :(得分:14)
好的,所以回答你自己的问题并不是一个好的形式,但我会记下我的想法,以防它启发任何人。 (我怀疑......)
如果monad可以被认为是“容器”,那么return
和join
都有非常明显的语义。 return
生成一个1元素容器,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)
现在,哪一个是读者功能,哪一个是计算下一个读者的功能......?
现在让我们试试好的State
monad。这里每个函数都将初始状态作为输入,但也返回一个新状态及其输出。
data State s x = State (s -> (s, x))
join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
这不是太难。它基本上都是运行然后运行。
我现在要停止打字了。请随意指出我的例子中的所有故障和拼写错误......: - /
答案 3 :(得分:11)
我发现很多关于monad的解释说“你不必知道关于类别理论的任何事情,真的,只要将monads想象为墨西哥卷饼/太空服/无论什么”。
真的,那个为我揭开神秘面纱的文章只是说了什么类别,在类别方面描述了monad(包括加入和绑定),并没有打扰任何虚假的隐喻:
我认为这篇文章非常易读,不需要太多的数学知识。
答案 4 :(得分:10)
致电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 <$>)
答案 5 :(得分:3)
询问Haskell 中的类型签名是什么就像询问Java 中的接口一样。
从字面意义上说,它“不是”。 (当然,你通常会有一些与之相关的目的,这主要是在你的脑海中,而且大部分都不在实现中。)
在这两种情况下,您都要在语言中声明符号的合法序列,这些符号将在以后的定义中使用。
当然,在Java中,我想你可以说一个接口对应一个类型签名,它将在VM中逐字实现。您可以通过这种方式获得一些多态性 - 您可以定义接受接口的名称,并且可以为接受不同接口的名称提供不同的定义。类似的事情发生在Haskell中,在那里你可以为一个接受一种类型的名称提供一个声明,然后为该名称提供一个处理不同类型的另一个声明。
答案 6 :(得分:1)