【问题标题】:What's so special about Monads in Kleisli category?Kleisli 类别中的 Monads 有什么特别之处?
【发布时间】:2019-04-06 04:27:22
【问题描述】:

相关问题是

  1. What is so special about Monads?

  2. bind can be composed of fmap and join, so do we have to use monadic functions a -> m b?

在第一个问题中:

Monad 有什么特别之处?

monad 是一种在(纯)函数式编程中大量使用的数学结构,基本上是 Haskell。但是,还有许多其他可用的数学结构,例如应用函子、强单子或幺半群。有些更具体,有些更通用。然而,单子更受欢迎。这是为什么呢?

回复问题的评论:

据我记得,monad 是由 Wadler 普及的,当时做 IO 而不用繁琐的 CPS 和解析而不显式传递状态的想法是巨大的卖点;这是一个非常激动人心的时刻。 A.F.A.I.R.,Haskell 没有做构造函数类,但 Gofer(Hugs 之父)做了。 Wadler 建议对 monad 重载列表推导,所以 do 表示法后来出现了。一旦 IO 成为 monadic,monad 对初学者来说就成了一件大事,将它们巩固为一个需要深入了解的主要事物。 Applicatives 会更好,Arrows 更通用,但它们来得较晚,IO 卖得很厉害。 – AndrewC 2013 年 5 月 9 日 1:34

@Conal 的回答是:

我怀疑对这一特定类型类别 (Monad) 的过度关注主要是历史上的侥幸。人们经常将IOMonad 联系在一起,尽管两者都是独立有用的想法(as are list reversal and bananas)。因为IO 很神奇(有实现但没有外延),而Monad 经常与IO 相关联,所以很容易陷入对Monad 的神奇思维。

首先,我同意他们的观点,我认为 Monads 的用处主要来自于 Functors,我们可以在结构中嵌入许多函数,而 Monads 是对函数组合鲁棒性的一点扩展 join :@ 987654333@ 避免嵌套类型。

在第二个问题中:

我们必须使用一元函数 a -> m b 吗?

网络上的许多教程仍然坚持使用单子函数,因为那是 Kleisli 三元组和单子定律。

还有很多类似的答案

我喜欢将这样的 m 视为“计划获得”的意思,其中“计划”涉及某种超出纯粹计算的额外交互。

在不需要Monad 的情况下,使用ApplicativeFunctor 或仅使用基本的纯函数通常更简单。在这些情况下,应该(并且通常)使用这些东西来代替Monad。例如:

ws <- getLine >>= return . words  -- Monad
ws <- words <$> getLine           -- Functor (much nicer)

要明确:如果没有 monad 是可能的,并且没有 monad 更简单且更具可读性,那么你应该在没有 monad 的情况下这样做!如果 monad 使代码比它需要的更复杂或更混乱,不要使用 monad! Haskell 拥有 monad 的唯一目的是使某些复杂的计算更简单、更易于阅读和更易于推理。如果这没有发生,你不应该使用 monad

阅读他们的回答,我想他们对 Monad 的特殊感觉源于 Haskell 社区恰好选择 Kleisli 类别中的 Monads 来解决他们的问题(IO 等)的历史事件

所以,再一次,我认为 Monads 的用处主要来自于 Functors,我们可以在结构中嵌入许多函数,而 Monads 是joinM(M(X)) -&gt; M(X) 对函数组合鲁棒性的一点扩展,以避免嵌套输入。

事实上,在 JavaScript 中我实现如下..

函子

console.log("Functor");
{
  const unit = (val) => ({
    // contextValue: () => val,
    fmap: (f) => unit((() => {
      //you can do pretty much anything here
      const newVal = f(val);
    //  console.log(newVal); //IO in the functional context
      return newVal;
    })()),
  });

  const a = unit(3)
    .fmap(x => x * 2)  //6
    .fmap(x => x + 1); //7
}

关键是我们可以在 Functor 结构中实现我们喜欢的任何东西,在这种情况下,我只是将其设置为 IO/console.log 值。

另外一点是,做这个 Monads 是绝对没有必要的。

单子

现在,基于上面的 Functor 实现,我添加了额外的 join: MMX =&gt; MX 功能以避免嵌套结构,这应该有助于复杂函数组合的稳健性。

功能与上面的 Functor 完全相同,请注意用法也与 Functor fmap 相同。这不需要bind 的“单子函数”(Kleisli 单子组合)。

console.log("Monad");
{
  const unit = (val) => ({
    contextValue: () => val,
    bind: (f) => {
      //fmap value operation
      const result = (() => {
        //you can do pretty much anything here
        const newVal = f(val);
        console.log(newVal);
        return newVal;
      })();
      //join: MMX => MX
      return (result.contextValue !== undefined)//result is MX
        ? result //return MX
        : unit(result) //result is X, so re-wrap and return MX
    }
  });
  //the usage is identical to the Functor fmap.
  const a = unit(3)
    .bind(x => x * 2)  //6
    .bind(x => x + 1); //7
}

单子定律

以防万一,Monad 的这种实现满足 monad 法则,而上面的 Functor 不满足。

console.log("Monad laws");
{
  const unit = (val) => ({
    contextValue: () => val,
    bind: (f) => {
      //fmap value operation
      const result = (() => {
        //you can do pretty much anything here
        const newVal = f(val);
        //console.log(newVal);
        return newVal;
      })();
      //join: MMX => MX
      return (result.contextValue !== undefined)
        ? result
        : unit(result)
    }
  });

  const M = unit;
  const a = 1;
  const f = a => (a * 2);
  const g = a => (a + 1);

  const log = m => console.log(m.contextValue()) && m;
  log(
    M(f(a))//==m , and f is not monadic
  );//2
  console.log("Left Identity");
  log(
    M(a).bind(f)
  );//2
  console.log("Right Identity");
  log(
    M(f(a))//m
      .bind(M)// m.bind(M)
  );//2
  console.log("Associativity");
  log(
    M(5).bind(f).bind(g)
  );//11
  log(
    M(5).bind(x => M(x).bind(f).bind(g))
  );//11

}

所以,这是我的问题。

我可能错了。

除了通过扁平化嵌套结构来实现函数组合的健壮性之外,Functor 不能做 Monad 可以做的事情吗?

Kleisli 类别中的 Monads 有什么特别之处?似乎有可能通过一些扩展来实现 Monads,以避免 Functor 的嵌套结构,并且没有 Kleisli 类别中的实体 a -&gt; m b 的单子函数。

谢谢。

编辑(2018-11-01)

阅读答案,我同意在应满足 Functor-laws 的 IdentityFunctor 中执行 console.log 是不合适的,因此我像 Monad 代码一样注释掉了。

所以,消除这个问题,我的问题仍然成立:

除了通过扁平化嵌套结构来实现函数组合的鲁棒性之外,Functor 不能做 Monad 可以做的事情吗?

Kleisli 类别中的 Monads 有什么特别之处?似乎有可能通过一些扩展来实现 Monads,以避免 Functor 的嵌套结构,并且没有 Kleisli 类别中的实体 a -&gt; m b 的单子函数。

@DarthFennec 的回答是:

“避免嵌套类型”实际上不是join 的目的,它只是一个巧妙的副作用。你说的方式听起来就像join 只是去掉了外部类型,但 monad 的值没有改变。

我相信“避免嵌套类型”不仅仅是一个简洁的副作用,而是范畴论中对 Monad 的“连接”的定义,

monad 的乘法自然变换 μ:T∘T⇒T 为每个对象 X 提供了一个态射 μX:T(T(X))→T(X)

monad (in computer science): Relation to monads in category theory

这正是我的代码所做的。

另一方面,

事实并非如此。 join 是 monad 的核心,它让 monad 能够做事

我知道很多人在 Haskell 中以这种方式实现 monad,但事实是,Haskell 中有 Maybe functor,没有join,或者有 Free monad join 从一开始就嵌入到定义的结构中。它们是用户定义函子以做事的对象。

因此,

您可以将函子视为基本上是一个容器。有一个任意的内部类型,围绕它的外部结构允许一些变化,一些额外的值来“装饰”你的内部值。 fmap 允许您处理容器内的东西,就像您正常处理它们的方式一样。这基本上是函子所能做的极限。

monad 是一个具有特殊能力的函子:fmap 允许您处理内部值,bind 允许您以一致的方式组合外部值。这比一个简单的函子要强大得多。

这些观察不符合 Maybe 函子和 Free monad 存在的事实。

【问题讨论】:

  • 我不明白你的问题。是的,扁平化嵌套结构正是 Monads 可以做到而 Functors 无法做到的。这就是为什么我们对两个不同的概念使用两个不同的术语。
  • result.contextValue !== undefined 上的分支毫无意义。您也不应该重载bind 来执行fmap。另外,我不明白你想用这个例子说明什么?
  • @Bergi 我写了“除了通过扁平化嵌套结构来实现函数组合的鲁棒性之外,有没有什么反例可以证明 Functor 不能做 Monad 可以做的事情?”和其他人明白了。抱歉,您的评论没有帮助。
  • 您一定是在过去一周左右在几个不同的帐户上不断提出类似问题的同一个人。您为什么不断创建新帐户?
  • 我不用几个账号来问。

标签: javascript haskell functional-programming monads


【解决方案1】:

除了通过扁平化嵌套结构来实现函数组合的鲁棒性之外,Functor 不能做 Monad 可以做的事情吗?

我认为这是重点:

Monads 是通过 join : M(M(X)) -&gt; M(X) 对函数组合的健壮性进行了一点扩展,以避免嵌套类型。

“避免嵌套类型”实际上不是join 的目的,它只是一个巧妙的副作用。你说它的方式使它听起来像join 只是剥离了外部类型,但 monad 的值没有改变。不是这种情况。 join 是 monad 的核心,它让 monad 能够做事

您可以将函子视为基本上是一个容器。有一个任意的内部类型,围绕它的外部结构允许一些变化,一些额外的值来“装饰”你的内部值。 fmap 允许你处理容器内的东西,就像你正常处理它们的方式一样。这基本上是函子所能做的极限。

monad 是一个具有特殊能力的函子:fmap 允许您处理内部值,bind 允许您以一致的方式组合外部值。这比一个简单的函子要强大得多。


关键是我们可以在 Functor 结构中实现我们喜欢的任何东西,在这种情况下,我只是简单地将其设置为 IO/console.log 值。

实际上,这是不正确的。您能够在这里进行 IO 的唯一原因是因为您使用的是 Javascript,并且您可以在任何地方进行 IO。在像 Haskell 这样的纯函数式语言中,IO 不能在这样的函子中完成。

这是一个粗略的概括,但在大多数情况下,将IO 描述为美化的State monad 是有用的。每个IO 动作都有一个额外的隐藏参数RealWorld(代表现实世界的状态),可能会读取或修改它,然后将其发送到下一个IO 动作。这个RealWorld 参数穿过链。如果某些内容被写入屏幕,那就是 RealWorld 被复制、修改和传递。但是“传递”是如何运作的呢?答案是join

假设我们想从用户那里读取一行,并将其打印回屏幕:

getLine :: IO String
putStrLn :: String -> IO ()

main :: IO ()
main = -- ?

假设IO 是一个函子。我们如何实现它?

main :: IO (IO ())
main = fmap putStrLn getLine

在这里,我们将putStrLn 提升为IO,得到fmap putStrLn :: IO String -&gt; IO (IO ())。如果你还记得,putStrLn 接受一个String 和一个隐藏的RealWorld 并返回一个修改后的RealWorld,其中String 参数被打印到屏幕上。我们已经用fmap 提升了这个函数,所以它现在需要一个IO(这是一个接受隐藏RealWorld 并返回修改后的RealWorldString 的操作),并返回相同的 io 操作,只是包裹在不同的值上(一个完全独立的操作,它也采用单独的隐藏 RealWorld 并返回 RealWorld)。即使在将getLine 应用到此函数之后,实际上也没有发生任何事情或被打印出来。

我们现在有一个main :: IO (IO ())。这是一个采用隐藏的RealWorld 的操作,并返回一个修改后的RealWorld 和一个单独的操作。第二个操作采用不同的RealWorld 并返回另一个修改后的RealWorld。这本身是没有意义的,它不会给你任何东西,也不会在屏幕上打印任何东西。需要发生的是,两个IO 动作需要连接在一起,以便一个动作返回的RealWorld 被作为另一个动作的RealWorld 参数输入。这样它就变成了一个连续的RealWorlds 链,随着时间的推移会发生变化。当两个IO 操作与join 合并时,就会发生这种“连接”或“链接”。


当然,join 会根据您使用的 monad 做不同的事情,但是对于 IOState 类型的 monad,这或多或少是在幕后发生的事情。在很多情况下,您正在做一些不需要join 的非常简单的事情,在这些情况下,很容易将 monad 视为函子或应用函子。但通常这还不够,在这些情况下我们使用 monad。


编辑:对 cme​​ts 和已编辑问题的回复:

我没有看到分类理论中对 Monads 的任何定义解释了这一点。我读到的关于 join 的内容仍然是 MMX =&gt; MX,这正是我的代码所做的。

你还能准确地说出String -&gt; String 的功能吗?它可能不会逐字返回输入、反转它、过滤它、附加到它、忽略它并返回一个完全不同的值,或者任何其他导致String 的东西?类型不决定函数做什么,它限制一个函数可以做什么。由于join 通常仅由其类型定义,因此任何特定的 monad 都可以执行该类型允许的任何事情。这可能只是剥离外层,或者可能是将两层合并为一个的一些极其复杂的方法。只要你从两层开始,到最后一层,没关系。该类型允许多种可能性,这是使 monad 一开始就如此强大的部分原因。

Haskell 中有 MaybeFunctor。那里没有“加入”或“绑定”,我想知道力量从何而来。 MaybeFunctor 和 MaybeMonad 有什么区别?

每个 monad 也是一个函子:一个 monad 只不过是一个函子,它也有一个 join 函数。如果您将joinbindMaybe 一起使用,则您将其用作monad,并且它具有monad 的全部功能。如果您不使用joinbind,而仅使用fmappure,则您将其用作函子,并且它仅限于执行函子可以做的事情。如果没有joinbind,就没有额外的monad 能力。

我相信“避免嵌套类型”不仅仅是一个简洁的副作用,而是范畴论中对 Monad “join”的定义

join 的定义是从嵌套 monad 到非嵌套 monad 的转换。同样,这可能意味着任何东西。说join 的目的是“避免嵌套类型”就像说+ 的目的是避免成对的数字。大多数操作以某种方式组合事物,但很少有这些操作仅仅为了组合事物而存在。重要的是合并是如何发生的

Haskell 中有 Maybe functor,它没有 join,或者有 Free monadjoin 从一开始就嵌入到定义的结构体。它们是用户定义函子以做事的对象。

我已经讨论过Maybe,以及当您仅将它用作函子时,它无法完成将其用作 monad 时可以做的事情。 Free 很奇怪,因为它是少数几个实际上并没有做任何事情的 monad 之一

Free 可用于将任何函子转换为 monad,这允许您使用 do 表示法和其他便利。然而,Free 的自负在于,join 并没有像其他 monad 那样组合您的操作,而是将它们分开,将它们插入到类似列表的结构中;这个想法是这个结构稍后被处理,动作由单独的代码组合。一种等效的方法是将处理代码移动到join 本身,但这会将函子变成一个monad,并且使用Free 没有任何意义。所以Free 起作用的唯一原因是因为它将单子的实际“做事”部分委托给了其他地方;它的join 选择将操作推迟到在 monad 之外运行的代码。这就像一个+ 运算符,它不是将数字相加,而是返回一个抽象语法树;然后可以在以后以任何需要的方式处理该树。

这些观察不符合 Maybe 函子和 Free monad 存在的事实。

你错了。如前所述,MaybeFree 完全符合我之前的观察结果:

  • Maybe 函子根本不具备与 Maybe monad 相同的表达能力。
  • Free monad 以它可能的唯一方式将函子转换为 monad:不实现 monadic 行为,而是简单地将其推迟到一些假定的处理代码。

【讨论】:

  • 好吧,““避免嵌套类型”实际上并不是 join 的目的,它只是一个简洁的副作用。你说它的方式听起来像是 join 只是剥离了外部类型,但是 monad 的值是不变的。事实并非如此。join 是 monad 的核心,它是允许 monad 做事的东西。我没有看到分类理论中对 Monads 的任何定义解释了这一点。我读到的关于 join 的内容仍然是 MMX =&gt; MX,这正是我的代码所做的。
  • "monad 是一个具有特殊能力的函子:其中 fmap 允许您处理内部值,bind 允许您以一致的方式组合外部值。这比简单的要强大得多函子。” Haskell 中有 MaybeFunctor。那里没有“加入”或“绑定”,我想知道力量从何而来。 MaybeFunctor 和 MaybeMonad 有什么区别?
【解决方案2】:

关键是我们可以在 Functor 结构中实现我们喜欢的任何东西,在这种情况下,我只是将其设置为 IO/console.log 值。

另外一点是,做这个 Monads 是绝对没有必要的。

问题是一旦你这样做了,你的仿函数就不再是仿函数了。函子应该保留身份和组成。对于 Haskell Functors,这些要求相当于:

fmap id = id
fmap (g . f) = fmap g . fmap f

这些法律保证fmap 所做的一切都是使用提供的函数来修改值——它不会在你背后做有趣的事情。对于您的代码,fmap(x =&gt; x) 应该什么都不做;相反,它会打印到控制台。

请注意,以上所有内容都适用于IO 函子:如果aIO 动作,则执行fmap f a 将不会有除a 已经有的I/O 效果之外。尝试编写与您的代码在精神上相似的东西可能是......

applyAndPrint :: Show b => (a -> b) -> a -> IO b
applyAndPrint f x = let y = f x in fmap (const y) (print y)

pseudoFmap :: Show b => (a -> b) -> IO a -> IO b
pseudoFmap f a = a >>= applyAndPrint f

...但这已经使用了Monad,因为我们的效果(打印结果)取决于先前计算的结果。

不用说,如果您愿意(并且您的类型系统允许),您可以编写忽略所有这些区别的代码。然而,有一个折衷:Functor 相对于Monad 的功率降低带来了额外的保证,即使用该接口的功能可以做什么和不可以做什么——这就是使这些区别首先有用的原因.

【讨论】:

  • @SmoothOperator 引用您的编辑:“Haskell 中有可能函子,但没有加入”。但是Maybe 有一个join,就像任何其他Monad 一样。
【解决方案3】:

你的“函子”显然不是函子,违反了恒等律和复合律:

console.log("Functor");
{
  const unit = (val) => ({
    // contextValue: () => val,
    fmap: (f) => unit((() => {
      //you can do pretty much anything here
      const newVal = f(val);
      console.log(newVal); //IO in the functional context
      return newVal;
    })()),
  });

  console.log("fmap(id) ...");
  const a0 = unit(3)
    .fmap(x => x);      // prints something
  console.log("         ≡ id?");
  const a1 = (x => x)(unit(3));   // prints nothing

  console.log("fmap(f) ∘ fmap(g) ...");
  const b0 = unit(3)
    .fmap(x => 3*x)
    .fmap(x => 4+x);     // prints twice
  console.log("                   ≡ fmap(f∘g)?");
  const b1 = unit(3)
    .fmap(x => 4+(3*x));    // prints once
}

【讨论】:

    【解决方案4】:

    评论过长:

    我建议暂时忘记 Kleisli 类别;我不相信他们与你的困惑有任何关系。

    此外,虽然我仍然不完全理解您的问题和断言,但一些可能有用的上下文:范畴论非常笼统和抽象;像 MonadFunctor 这样的概念,因为它们存在于 haskell )。

    作为一般规则,事物变得越具体(越不抽象),您拥有的力量就越大:如果我告诉您您有交通工具,那么您就知道您拥有可以将您从一个地方带到另一个地方的东西,但是你不知道它有多快,你不知道它是否可以在陆地上行驶,等等。如果我告诉你你有一艘快艇,那么你可以做的事情和推理的事情就会打开一个更大的世界(你可以用它来捕鱼,你知道它不会把你从纽约带到丹佛)。

    当你说:

    Kleisli 类别中的 Monads 有什么特别之处?

    ...我相信您误以为 Haskell 中的 MonadFunctor 的概念在某种程度上更多相对于类别理论具有限制性,但是,正如我上面试着类比解释,反之亦然。

    您的代码是同样的错误思维:您为快艇(它是一种车辆)建模并声称它表明所有车辆都很快并且可以在水上行驶。

    【讨论】:

      猜你喜欢
      • 2011-04-18
      • 2017-02-19
      • 2016-09-30
      • 2017-05-16
      • 2011-07-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多