首页 文章

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

提问于
浏览
56

Monad通常以 returnbind 轮流解释 . 但是,我收集你也可以用 join (和 fmap ?)来实现 bind

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

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

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

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

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


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

Monad join function

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

7 回答

  • 9

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

    Monad explained in one picture.

  • 9

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

    • 如果你已经有了一个值,那么你有一个策略来产生一个值( return :: v -> m v ),除了产生你拥有的值之外什么都没有;

    • 如果你有一个将一种 Value 转换成另一种 Value 的函数,只需等待策略提供其 Value ,然后转换它,就可以将其提升为策略( fmap :: (v -> u) -> m v -> m u );

    • 如果你有一个生成策略来生成一个值的策略,那么你可以构建一个策略来产生一个值( join :: m (m v) -> m v ),它遵循外部策略,直到它产生内部策略,然后遵循内部策略一直到a值 .

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

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

    ......代表通过掷硬币来制作东西的策略 . 如果策略是 Leaf v ,那么你的 v ;如果策略是 Node h t ,如果硬币显示为"heads", t ,如果它是"tails",则抛出一枚硬币继续按策略 h .

    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

    我们正在利用的是"strategy to produce a value"本身可以被视为一种 Value . 在Haskell中,策略作为值的嵌入是沉默的,但在英语中,我使用引号来区分使用策略而不仅仅是谈论它 . join 运算符表示策略"somehow produce then follow a strategy",或“如果您被告知策略,则可以使用它” .

    (Meta.I 'm not sure whether this 1123334 approach is a suitably generic way to think about monads and the value/computation distinction, or whether it'只是另一个糟糕的比喻 . 我确实发现叶子标记的树状类型是直觉的有用来源,这可能不是一个惊喜,因为它们是免费的monad,只有足够的结构才能成为monad ,但没有更多 . )

    PS“绑定”的类型

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

    说“如果你有一个 生产环境 v 的策略,并且每个v产生一个后续策略来生成 w ,那么你就有了生成 w 的策略” . 我们怎样才能从 join 中获取?

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

    我们可以通过 v2mw 重新标记我们的 v 生产环境 策略,而不是每个 v 值生成 w 生成策略,该策略从它开始 - 准备好 join

  • 13
    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
    
  • 1

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

    如果monad可以被认为是"container",那么 returnjoin 都有非常明显的语义 . return 生成1个元素的容器, join 将容器容器转换为单个容器 . 没什么难的 .

    因此,让我们专注于更自然地被认为是"actions"的monad . 在这种情况下, m x 是某种动作,当你"execute"它时会产生 x 类型的值 . return x 没有什么特别的,然后产生 x . fmap f 采取产生 x 的操作,并构造一个计算 x 的操作,然后对其应用 f ,并返回结果 . 到现在为止还挺好 .

    很明显,如果 f 本身生成一个动作,那么你最终得到的是 m (m x) . 也就是说,计算另一个动作的动作 . 在某种程度上,比起采取行动和"function that produces an action"等的 >>= 函数,你可能更容易理解 .

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

    这似乎是中心思想 . 要实现 join ,您希望运行一个操作,然后再为您提供另一个操作,然后运行该操作 . (无论"run"对于这个特殊的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

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

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

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

  • 90

    调用 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

  • 25

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

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

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

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

相关问题