克莱斯里类别的单子有什么特别之处?

时间:2018-11-01 21:53:43

标签: javascript haskell functional-programming monads

相关问题是

  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?

第一个问题:

  

Monads有什么特别之处?

     

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

回答问题的评论:

  

据我所知,单子被Wadler所普及,当时没有乏味的CPS进行IO和没有明确的状态传递就进行解析的想法是巨大的卖点。那是一个非常激动人心的时刻。 A.F.A.I.R.,Haskell没有构造函数类,但是Gofer(Hugs的父亲)做了。 Wadler提出了monad的重载列表理解功能,因此后来出现了do表示法。一旦IO变成了monadic,monads对初学者来说就变得很重要,巩固了他们作为grok的主要目标。可能的话,应用程序要好得多,Arrows则更通用,但后来出现了,IO很难销售单子。 – AndrewC '13年5月9日,下午1:34

@Conal的答案是:

  

我怀疑与其他许多类型相比,对这一特定类型类(Monad)的过多关注主要是历史上的fl幸。人们经常将IOMonad相关联,尽管两者是独立有用的想法(as are list reversal and bananas)。因为IO是神奇的(具有实现但没有符号),并且Monad通常与IO相关联,所以很容易陷入关于Monad的神奇思考。

首先,我同意他们的观点,我认为Monads的用处主要来自Functors,因为我们可以在结构中嵌入许多功能,而Monads则通过join扩展了功能组合的健壮性:M(M(X)) -> M(X)以避免嵌套类型。

第二个问题:

  

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

     

如此多的网上教程仍然坚持使用monadic函数,因为那是Kleisli三元组和monad-laws。

还有许多类似的答案

  

我喜欢将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类别的Monad来解决他们的问题(IO等)

所以,再次,我认为Monads的有用性主要来自Functors,我们可以在结构中嵌入许多功能,而Monads则通过joinM(M(X)) -> 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。

Monad

现在,基于上面的Functor实现,我添加了额外的join: MMX => 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

}

所以,这是我的问题。

我可能是错的。

是否有任何反例表明Functors除了通过使嵌套结构变平坦来增强功能组合的健壮性之外,不能做Monads可以做的事情?

Kleisli类别的Monads有何特别之处?似乎有可能通过稍微扩展来实现Monad来避免Functor的嵌套结构,并且不使用属于Kleisli类别的实体的monadic函数a -> m b

谢谢。

编辑(2018-11-01)

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

因此,消除该问题,我的问题仍然存在:

是否有任何反例表明Functors除了通过使嵌套结构变平坦来增强功能组合的健壮性之外,无法执行Monads的功能?

Kleisli类别的Monad有何特别之处?似乎有可能通过稍微扩展来实现Monad来避免Functor的嵌套结构,并且不使用属于Kleisli类别中实体的monadic函数a -> m b

@DarthFennec的回答是:

  

“避免嵌套类型”实际上并不是join的目的,它只是一个很好的副作用。使用它的方式听起来像join只是剥离了外部类型,但monad的值不变。

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

  

单子的乘法自然变换μ: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中有也许仿函数,它没有join,或者有免费的monad join从一开始就嵌入到已定义的结构中。它们是用户定义执行任务的功能键 的对象。

因此

  

您可以认为函子基本上是一个容器。有一个任意的内部类型,它周围是一个外部结构,该结构允许一些差异,一些额外的值来“装饰”您的内部值。 fmap允许您以正常方式处理容器内部的内容。这基本上是您可以使用函子的限制。

     

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

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

4 个答案:

答案 0 :(得分:3)

  

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

     

另一点是,绝对不需要这样做Monads。

问题在于,一旦执行此操作,函子就不再是函子。函子应保留身份和组成。对于Haskell Functor,这些要求总计为:

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

这些法律可以保证所有fmap都在使用提供的函数来修改值-它不会在背后做任何有趣的事情。对于您的代码,fmap(x => x)不应执行任何操作;而是打印到控制台。

请注意,以上所有内容均适用于IO仿函数:如果aIO动作,则执行fmap f a不会产生I / O效果a已经拥有。在精神上写出与您的代码相似的东西的一个刺痛可能是...

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的降低的功能还额外保证了使用该接口可以执行和不能执行的功能-这就是使区别有用的原因首先。

答案 1 :(得分:3)

  

是否有任何反例表明Functors除了通过使嵌套结构变平坦来增强功能组合的健壮性之外,不能做Monads可以做的事情?

我认为这很重要:

  

Monads通过加入M(M(X)) -> M(X)来扩大函数组合的健壮性,以避免嵌套类型。

“避免嵌套类型”实际上并不是join的目的,它只是一个很好的副作用。使用它的方式听起来像join只是剥离了外部类型,但monad的值不变。不是这种情况。 join是monad的心脏,这就是让monad 做事的原因。

您可以认为函子基本上是一个容器。有一个任意的内部类型,它周围是一个外部结构,该结构允许一些差异,一些额外的值来“装饰”您的内部值。 fmap允许您以正常方式处理容器内部的内容。这基本上是您可以使用函子的限制。

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


  

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

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

这是一个粗略的概括,但是在大多数情况下,将IO描述为美化的State单子很有用。每个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 -> IO (IO ())。如果您还记得,putStrLn会使用String隐藏的RealWorld 并返回修改后的RealWorld,其中String参数为打印到屏幕上。我们使用fmap取消了此功能,因此它现在需要一个IO(这是一个操作,它需要一个隐藏的RealWorld,并返回修改后的RealWorld和一个String,并返回相同io操作,只是包裹在另一个值上(一个完全独立的操作,该操作也需要一个单独的隐藏RealWorld并返回一个{{1} }。即使在将RealWorld应用于此功能之后,实际上也不会发生任何事情或将其打印出来。

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


当然,join会根据您使用的单子而做不同的事情,但是对于joinIO型单子来说,这或多或少是在罩。在很多情况下,您所做的非常简单的操作不需要State,在这些情况下,将monad视为函子或应用函子很容易。但是通常这还不够,在这种情况下,我们使用单子。


编辑:对评论和已编辑问题的回复:

  

我没有看到在categort理论中对Monads的任何定义可以解释这一点。我读到的所有关于join的内容都是stil join,而这正是我的代码所做的。

您还能准确说明函数MMX => MX的作用吗?可能不按原样返回输入,反转,过滤,追加,忽略它并返回完全不同的值,或者导致String -> String的其他任何结果吗?类型不能确定函数的功能,它会限制函数的功能。由于String通常仅由其类型定义,因此任何特定的monad都可以执行该类型允许的任何操作。这可能只是剥离外层,或者可能是将两层合并为一层的极其复杂的方法。只要从两层开始,到最后一层,都没有关系。这种类型提供了多种可能性,这是使monad如此强大的原因之一。

  

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

每个monad都是函子:monad仅仅是具有join功能的函子。如果您将joinjoinbind一起使用,则您将其用作单子,并且具有单子的全部功能。如果您不使用Maybejoin,而仅使用bindfmap,则表示您将其用作函子,并且它仅限于做仿函数可以做到。如果没有purejoin,则没有额外的单子电源。

  

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

bind的定义是从嵌套monad到非嵌套monad的转换。同样,这可能意味着任何内容。说join的目的是“避免嵌套类型”,就像说join的目的是避免数字对。大多数操作以某种方式组合事物,但是仅出于将事物组合在一起的目的而存在的操作很少。重要的是结合如何发生

  

在Haskell中有也许是仿函数,它没有+,或者有 Free monad 是从第一个嵌入的join放入定义的结构。它们是用户定义执行任务的功能键 的对象。

我已经讨论过join,以及当您仅将其用作函子时,如何将其用作monad不能做它能做的事情。 Maybe很奇怪,因为它是实际上不做任何事情的少数几个单子。

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

  

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

您不正确。如前所述,+Maybe非常适合我以前的观察结果:

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

答案 2 :(得分:1)

您的“ functor”显然不是仿函数,同时违反了身份和组成法则:

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
}

答案 3 :(得分:1)

评论太长

我建议暂时不要忘记Kleisli类别;我认为他们与您的困惑无关。

虽然我仍然不完全理解您的问题和主张,但有些上下文可能会有用:类别理论非常笼统和抽象; haskell中存在的MonadFunctor之类的概念(必然)有些 less 通用和 less 抽象(例如,类别的概念) “ Hask”)。

一般来说,事物变得越具体(越不抽象),您拥有的力量就越多:如果我告诉您拥有车辆,那么您知道自己拥有可以将您从一个地方带到另一个地方的东西,但是您不知道它有多快,您是否知道它是否可以在陆地上行驶,等等。如果我告诉您有一艘快艇,那么您可以做的事和推理的整个世界就更大了(您可以用它抓鱼,您知道它不会把您从纽约带到丹佛)。

当你说:

  

Kleisli类别的Monads有何特别之处?

......我相信您犯了一个错误,即怀疑haskell中MonadFunctor的概念在某种程度上相对于类别理论具有限制性但是,正如我试图通过上面的类比解释一样,情况恰恰相反。

您的代码也有类似的错误思维:您对快艇(这是一种车辆)进行建模,并声称它表明所有车辆都快并且可以在水上行驶。