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

时间:2012-06-27 20:50:46

标签: haskell monads

Monad通常以returnbind轮流解释。但是,我知道您还可以bind(和join?)来实施fmap

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

但我并不完全确定我理解join的工作方式。显然,它有[Haskell]类型

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

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

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


糟糕。看起来之前有人问过:

Monad join function

有人可以使用returnfmapjoin草拟一些常见monad的实现吗? (即,根本没有提到>>=。)我想也许这可能有助于它沉入我愚蠢的大脑......

7 个答案:

答案 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的策略。

tree of trees

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

显然join制定了一个策略Int

enter image description here

我们正在利用的是“产生价值的策略”本身可以被视为一种价值。在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可以被认为是“容器”,那么returnjoin都有非常明显的语义。 return生成一个1元素容器,join将容器容器转换为单个容器。没什么难的。

因此,让我们专注于更自然地被认为是“行动”的单子。在这种情况下,m x是某种动作,当您“执行”它时会生成x类型的值。 return x没有什么特别之处,然后产生xfmap 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 <$>)

enter image description here

答案 5 :(得分:3)

询问Haskell 中的类型签名是什么就像询问Java 中的接口一样。

从字面意义上说,它“不是”。 (当然,你通常会有一些与之相关的目的,这主要是在你的脑海中,而且大部分都不在实现中。)

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

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

答案 6 :(得分:1)

这是莫纳德在一张照片中解释的。绿色类别中的2个功能不可组合,当映射到蓝色类别时(严格来说,它们是一个类别),它们变得可组合。 Monad将T -> Monad<U>类型的函数转换为Monad<T> -> Monad<U>的函数。

Monad explained in one picture.