使用Join()而不是Bind()的Monads

Mat*_*hid 62 monads haskell

Monad通常用return和来解释bind.不过,我猜想你也可以实现bind在以下方面join(和fmap?)

在缺乏一流功能的编程语言中,使用起来bind非常难以捉摸.join另一方面,看起来很容易.

但是,我并不完全确定我理解它是如何join运作的.显然,它有[Haskell]类型

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

对于monad列表,这显然很简单concat.但是对于一般的monad来说,这种方法在操作上实际上做了什么?我看到它对类型签名的作用,但我试图弄清楚我是如何在Java或类似的东西中写出这样的东西.

(实际上,这很容易:我不会.因为仿制药已经坏了.;-)但原则问题仍然存在......)


哎呀.看起来之前有人问过:

Monad加入功能

可能有人勾画出使用普通的单子一些实现return,fmapjoin?(即,根本没有提到>>=.)我想也许这可能有助于它沉入我愚蠢的大脑......

pig*_*ker 96

如果没有管道隐喻的深度,我可能会建议将典型的monad m作为"生成策略的策略",因此类型m value是产生价值的第一类"策略".不同的计算或外部交互概念需要不同类型的策略,但一般概念需要一些规则的结构才有意义:

  • 如果你已经有了一个值,那么你有一个策略来生成一个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)
Run Code Online (Sandbox Code Playgroud)

......代表通过掷硬币来制作东西的策略.如果策略是Leaf v,那就是你的v; 如果策略是Node h t,h如果硬币显示"头",t如果它是"尾巴" ,你就掷硬币并继续策略.

instance Monad Tree where
  return = Leaf
Run Code Online (Sandbox Code Playgroud)

策略生成策略是一棵带有树标记叶子的树:代替每一片这样的叶子,我们可以只在树上标记它的树...

  join (Leaf tree) = tree
  join (Node h t)  = Node (join h) (join t)
Run Code Online (Sandbox Code Playgroud)

...当然,我们fmap只有相关的叶子.

instance Functor Tree where
  fmap f (Leaf x)    = Leaf (f x)
  fmap f (Node h t)  = Node (fmap f h) (fmap f t)
Run Code Online (Sandbox Code Playgroud)

这是制定生产战略的策略Int.

树的树

掷硬币:如果它是"头",则抛出另一枚硬币来决定两种策略(分别产生"掷硬币生产0或生产1"或"生产2"); 如果它是"尾巴"产生第三个("投掷硬币生产3或掷硬币4或5").

显然join,制定战略的目标正是如此Int.

在此输入图像描述

我们正在利用的是,"产生价值的战略"本身可以被视为一种价值.在Haskell中,策略作为值的嵌入是沉默的,但在英语中,我使用引号来区分使用策略而不仅仅是谈论它.该join运营商表示战略"不知何故产生然后按照战略",或者"如果你告诉一个战略,你便可以使用它."

(Meta.我不确定这种"策略"方法是否是一种适当的通用方式来考虑monad和值/计算的区别,或者它是否只是另一个糟糕的比喻.我确实发现叶子标记的树状类型是有用的直觉的来源,也许不是一个惊喜,因为它们是自由的单子,只有足够的结构才能成为单子,但不多了.)

PS"绑定"的类型

(>>=) :: m v -> (v -> m w) -> m w
Run Code Online (Sandbox Code Playgroud)

他说:"如果你有一个策略来制作一个v,并且每个va后续策略都要产生一个w,那么你就有了制作一个策略w".我们怎样才能捕捉到这一点join

mv >>= v2mw = join (fmap v2mw mv)
Run Code Online (Sandbox Code Playgroud)

我们可以重新v制定我们的生产策略v2mw,从而生成代替每个v价值的w生产策略 - 随时准备join!

  • @Jamil如果你发现这个有用,你应该看看Dan Piponi的文章[Monads Are Trees With Grafting](https://dl.dropbox.com/u/828035/Monads/monads.pdf).它显示了如何以这种方式查看*all*monads. (11认同)
  • @Jamil(谢谢!)在列表monad中,"生成值的策略"包括提供值的选择.`concat`将选择事物之间的选择变成了一种选择,例如将一条满是商店的街道(首先选择商店,然后选择东西)变成一个大超市. (6认同)
  • 是的,它确实.我们有`v2mw :: v - > mw`和`fmap ::(a - > b) - > ma - > mb`所以`fmap v2mw` typechecks用`a = v`和`b = mw`给出类型` mv - > m(mw)`,这就是之后`join`给你`mw`的原因. (2认同)
  • 就自然语言的隐喻而言,“产生价值的策略”听起来不像“上下文中的价值”那么笨拙。 (2认同)
  • 克里斯·泰勒(Chris Taylor)的链接来自[Dan Piponi](http://blog.sigfpe.com/2010/01/monads-are-trees-with-grafting.html)的博客。现在链接已死。丹·皮波尼(Dan Piponi)提供了[更新后的链接](https://github.com/dpiponi/grafting3/blob/master/monads.pdf)。 (2认同)

Dan*_*ner 28

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
Run Code Online (Sandbox Code Playgroud)

  • 好吧,这就是我要的。;-)有点……虽然真是让人难以理解。也许如果我真的很慢地看它的话 (3认同)

Mat*_*hid 15

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

如果一个单子可以被看作是一个"容器",那么这两个returnjoin具有很明显的语义.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
Run Code Online (Sandbox Code Playgroud)

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

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

join (Reader f) = Reader (\ s -> let Reader g = f s in g s)
Run Code Online (Sandbox Code Playgroud)

那......无痛苦.A Reader实际上只是一个采用全局状态的函数,然后才返回其结果.因此,对于取消堆栈,您将全局状态应用于外部操作,后者返回一个新操作Reader.然后,您也可以将状态应用于此内部函数.

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

Reader f >>= g = Reader (\ s -> let x = f s in g x)
Run Code Online (Sandbox Code Playgroud)

现在,哪一个是读者功能,哪一个是计算下一个读者的功能......?

现在让我们尝试一下古老的Statemonad吧.这里每个函数都将初始状态作为输入,但也返回一个新状态及其输出.

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

join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
Run Code Online (Sandbox Code Playgroud)

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

我现在要打算停止打字了.随意指出我的例子中的所有故障和拼写错误...: - /

  • 我只发现了一个错误 - 缺少'join(Just mx)`中的parens.此外,回答你自己的问题也不错,参见 http://blog.stackoverflow.com/2011/07/its-ok-to-ask-and-answer-your-own-questions/ (5认同)

小智 12

我发现很多关于monad的解释说"你不必知道关于类别理论的任何事情,真的,只要将monads想象为墨西哥卷饼/太空服/无论什么".

真的,那个为我揭开神秘面纱的文章只是说了什么类别,在类别方面描述了monad(包括加入和绑定),并没有打扰任何虚假的比喻:

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


Wil*_*ess 12

呼叫产生价值所以它是一个很自然的事情用取回值.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
Run Code Online (Sandbox Code Playgroud)

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

    ((g <=< f) =<<)  =  (g =<<) . (f =<<)  =  join . (g <$>) . join . (f <$>)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述


rdm*_*rdm 5

询问 Haskell 中的类型签名的作用就像询问 Java 中的接口的作用一样

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

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

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