简单英语中的Monad?(适用于没有FP背景的OOP程序员)

对于OOP程序员来说(没有任何函数式编程背景),什么是monad?

它解决了什么问题,它最常用的地方是什么?

更新

为了澄清我想要的那种理解,假设你正在将一个包含monad的FP应用程序转换为一个OOP应用程序。你会怎么做来将monad的职责移植到OOP应用程序?

171817 次浏览

wikipedia

在函数式编程中,monad是一种抽象数据类型,用于表示计算(而不是域模型中的数据)。单子允许程序员链接动作一起建造一条管道,其中每个动作都装饰着提供的附加处理规则由monad编写的程序功能性风格可以利用monads来构建程序包括顺序操作,1[2]或定义任意控制流(如处理并发,继续,或异常)。

形式上,monad是由定义两个操作(bind和返回)和一个类型构造函数M必须满足多个属性才能允许正确的组成一元函数(即使用monad中的值作为其参数)。返回操作需要一个普通类型的值,并将其装入M型一元容器。bind操作执行反向过程,提取容器的原始值和将其传递给关联的下一个管道中的函数。

程序员将编写monadic定义数据处理的函数管道。单子充当框架,因为它是一种可重用的行为这决定了顺序中特定一元函数管道被调用,并管理所有要求的卧底工作计算。[3]绑定和返回在管道中交错的操作员将在每次一元之后执行函数返回控制,并将照顾特定方面由monad处理。

我相信这很好地解释了这一点。

更新:这个问题是一个非常长的博客系列的主题,你可以在单子阅读-谢谢你的好问题!

对于OOP程序员来说(没有任何函数式编程背景),什么是monad?

一个monad是类型的“放大器”遵守某些规则它提供了某些操作

首先,什么是“类型放大器”?我指的是一些系统,它可以让你获取一个类型并将其转换为更特殊的类型。例如,在C#中,考虑Nullable<T>。这是一个类型放大器。它允许你获取一个类型,比如int,并为该类型添加一个新功能,即现在它可以为空,而以前它不能。

作为第二个例子,考虑IEnumerable<T>。它是一个类型放大器。它允许您获取一个类型,例如string,并为该类型添加一个新功能,即您现在可以用任意数量的单个字符串制作一系列字符串。

什么是“某些规则”?简而言之,有一种合理的方法可以让底层类型的函数在放大的类型上工作,以便它们遵循函数组合的正常规则。例如,如果你有一个关于整数的函数,比如

int M(int x) { return x + N(x * 2); }

然后Nullable<int>上的相应函数可以使那里的所有运算符和调用“以与以前相同的方式”协同工作。

(这是令人难以置信的模糊和不精确;你要求的解释没有假设任何关于功能组成的知识。)

什么是“操作”?

  1. 有一个“单元”操作(有时令人困惑地称为“返回”操作),它从普通类型中获取一个值并创建等效的一元值。这本质上提供了一种方法来获取未放大类型的值并将其转换为放大类型的值。它可以作为OO语言中的构造函数来实现。

  2. 有一个“bind”操作接受一元值和一个可以转换值的函数,并返回一个新的一元值。bind是定义monad语义学的关键操作。它让我们将对未放大类型的操作转换为对放大类型的操作,这符合前面提到的函数组合规则。

  3. 通常有一种方法可以将未放大的类型从放大的类型中恢复出来。严格来说,这个操作不需要有monad。(尽管如果你想有一个comonad,这是必要的。我们不会在本文中进一步考虑这些。)

同样,以Nullable<T>为例。你可以使用构造函数将int变成Nullable<int>。C#编译器为你处理大多数可为空的“提升”,但如果没有,提升转换很简单:一个操作,比如,

int M(int x) { whatever }

被转化为

Nullable<int> M(Nullable<int> x){if (x == null)return null;elsereturn new Nullable<int>(whatever);}

Nullable<int>转换为int是使用Value属性完成的。

函数转换是关键位。请注意可空操作的实际语义学-对null的操作传播null-是如何在转换中捕获的。我们可以泛化这一点。

假设你有一个从intint的函数,就像我们最初的M一样。你可以很容易地把它变成一个接受int并返回Nullable<int>的函数,因为你可以通过可空构造函数运行结果。现在假设你有这个高阶方法:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func){if (amplified == null)return null;elsereturn func(amplified.Value);}

看看你能做些什么?任何接受#0并返回#0的方法,或者接受#0并返回#3的方法现在都可以将可为空的语义学应用于它

再者:假设你有两种方法

Nullable<int> X(int q) { ... }Nullable<int> Y(int r) { ... }

你想把它们组合起来:

Nullable<int> Z(int s) { return X(Y(s)); }

也就是说,ZXY的组合。但是你不能这样做,因为X接受intY返回Nullable<int>。但是因为你有“绑定”操作,你可以这样做:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

monad上的绑定操作使放大类型上的函数组合工作。我在上面手摇的“规则”是monad保留了正常函数组合的规则;与身份函数组合导致原始函数,组合是关联的,等等。

在C#中,“绑定”被称为“选择多个”。看看它在序列monad上的工作原理。我们需要做两件事:将值转换为序列并对序列进行绑定操作。作为奖励,我们还有“将序列转换回值”。这些操作是:

static IEnumerable<T> MakeSequence<T>(T item){yield return item;}// Extract a valuestatic T First<T>(IEnumerable<T> sequence){// let's just take the first oneforeach(T item in sequence) return item;throw new Exception("No first item");}// "Bind" is called "SelectMany"static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func){foreach(T item in seq)foreach(T result in func(item))yield return result;}

可以为空的monad规则是“将两个产生null的函数组合在一起,检查内部函数是否为null;如果是,则产生null,如果不是,则调用具有结果的外部函数”。

序列monad规则是“将两个产生序列的函数组合在一起,将外部函数应用于内部函数产生的每个元素,然后将所有产生的序列连接在一起”。monad的基本语义学在Bind/SelectMany方法中被捕获;这是告诉你monad真正意味着的方法。

我们可以做得更好。假设你有一个int序列,以及一个接受int并产生字符串序列的方法。我们可以泛化绑定操作以允许组合接受和返回不同放大类型的函数,只要一个的输入匹配另一个的输出:

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func){foreach(T item in seq)foreach(U result in func(item))yield return result;}

所以现在我们可以说“将这一堆单个整数放大为整数序列。将这个特定的整数转换为一堆字符串,放大为字符串序列。现在将这两个操作放在一起:将这一堆整数放大为所有字符串序列的串联。”单子允许你组成放大。

它解决了什么问题,它最常用的地方是什么?

这相当于问“单例模式解决了什么问题?”,但我会试一试。

单子通常用于解决以下问题:

  • 我需要为此类型创建新功能,并且仍然组合此类型上的旧功能以使用新功能。
  • 我需要捕获类型上的一系列操作并将这些操作表示为可组合对象,构建越来越大的组合,直到我有正确的一系列操作表示,然后我需要开始从事物中获取结果
  • 我需要用一种讨厌副作用的语言清晰地表示副作用操作

C#在设计中使用monad。如前所述,可空模式非常类似于“可能monad”。LINQ完全由monad构建;SelectMany方法是操作组合的语义工作。(埃里克·梅耶尔喜欢指出每个LINQ函数实际上都可以由SelectMany实现;其他一切只是方便。)

为了澄清我想要的那种理解,假设你正在将一个包含monad的FP应用程序转换为一个OOP应用程序。你会怎么做来将monad的职责移植到OOP应用程序中?

大多数OOP语言没有足够丰富的类型系统来直接表示monad模式本身;你需要一个支持比泛型类型更高类型的类型的类型系统。所以我不会尝试这样做。相反,我会实现代表每个monad的泛型类型,并实现代表你需要的三种操作的方法:将值转换为放大值,(也许)将放大值转换为值,以及将未放大值的函数转换为放大值的函数。

一个很好的起点是我们如何在C#中实现LINQ。研究SelectMany方法;它是理解序列单子在C#中如何工作的关键。这是一个非常简单的方法,但非常强大!


建议进一步阅读:

  1. 为了在C#中对monads进行更深入和理论上合理的解释,我强烈推荐我(Eric Lippert的)同事Wes Dyer关于这个主题的文章。这篇文章是当monads最终为我“点击”时向我解释的。
  2. 很好地说明了为什么你可能想要一个(在示例中使用Haskell)左右的monad。
  3. 排序,将上一篇文章“翻译”为JavaScript。

monad是一种封装值的数据类型,本质上可以对其应用两个操作:

  • return x创建封装x的monad类型的值
  • m >>= f(读作“绑定运算符”)将函数f应用于monadm中的值

这就是monad。有还有一些技术细节,但基本上这两个操作定义了monad。真正的问题是,“什么是monad确实?”,这取决于monad-列表是monad,Maybes是monad,IO操作是monad。当我们说这些东西是monad时,这意味着它们具有return>>=的monad接口。

如果您曾经使用过Powershell,Eric描述的模式听起来应该很熟悉。PowerShell cmdlet是单子;功能组合由一条管道表示。

杰弗里·索弗对埃里克·梅耶尔的采访更详细。

我想说,与monad最接近的OO类比是“命令模式”。

在命令模式中,您将普通语句或表达式包装在命令对象中。命令对象公开了执行包装语句的执行方法。因此,语句被转换为第一类对象,可以随意传递和执行。命令可以是组成,因此您可以通过链接和嵌套命令对象来创建程序对象。

命令由一个单独的对象调用者执行。使用命令模式(而不仅仅是执行一系列普通语句)的好处是,不同的调用者可以对命令的执行方式应用不同的逻辑。

命令模式可用于添加(或删除)宿主语言不支持的语言功能。例如,在假设的无异常OO语言中,您可以通过向命令公开“try”和“扔”方法来添加异常语义学。当命令调用抛出时,调用者会回溯命令列表(或树),直到最后一次“try”调用。相反,您可以通过捕获每个命令抛出的所有异常并将它们转换为错误代码,然后传递给下一个命令,从而从语言中删除异常语义(如果您相信例外是不好的)。

甚至更花哨的执行语义学,如事务、非确定性执行或延续,都可以用一种不支持它的语言来实现。如果你仔细想想,这是一个非常强大的模式。

现在在现实中,命令模式并没有像这样用作通用语言功能。将每个语句转换为单独的类的开销会导致无法承受的样板代码量。但原则上,它可以用来解决与单子在fp中用于解决的问题相同的问题。

对于OOP程序员来说理解(没有任何功能编程背景),什么是莫奈?

它解决了什么问题?最常见的地方是什么?最常见的地方是什么?

就面向对象编程而言,monad是一个接口(或者更可能是一个混合),由类型参数化,有两个方法,returnbind描述:

  • 如何注入一个值来获得一个注入值的一元值类型;
  • 如何使用一个函数从一个非一元1,一元值。

它解决的问题与您期望从任何界面解决的问题类型相同,即:“我有一堆不同的类,它们做不同的事情,但似乎以一种具有潜在相似性的方式做这些不同的事情。我怎么能描述它们之间的相似性,即使类本身并不是比'Object'类本身更接近的任何东西的子类型?“

更具体地说,Monad的“接口”类似于IEnumeratorIIterator,因为它接受了一个本身接受一个类型的类型。Monad的主要“点”是能够基于内部类型连接操作,甚至达到拥有新的“内部类型”的程度,同时保持甚至增强主类的信息架构。

您最近有一个由克里斯多夫联盟(2010年7月12日)编写的演示文稿“Monadologe----对类型焦虑的专业帮助”,这对延续和单子的主题非常有趣。
这个(幻灯片共享)演示文稿的视频实际上是可在vimeo上找到
在这个一小时的视频中,Monad部分从大约37分钟开始,从58张幻灯片中的第42张幻灯片开始。

它被描述为“函数式编程的领先设计模式”,但示例中使用的语言是Scala,它既是面向对象的又是函数式的。
您可以在博客文章“单子-在Scala中抽象计算的另一种方法”中阅读有关Monad in Scala的更多信息,从Debasish Ghosh(2008年3月27日)开始。

类型构造函数 M如果支持以下操作,则为monad:

# the return functiondef unit[A] (x: A): M[A]
# called "bind" in Haskelldef flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]
# Other two can be written in term of the first two:
def map[A,B] (m: M[A]) (f: A => B): M[B] =flatMap(m){ x => unit(f(x)) }
def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =flatMap(ma){ x => mb }

例如(在Scala中):

  • Option是一个monad
def unit[A] (x: A): Option[A] = Some(x)
def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] =m match {case None => Nonecase Some(x) => f(x)}
  • List是Monad
def unit[A] (x: A): List[A] = List(x)
def flatMap[A,B](m:List[A])(f:A =>List[B]): List[B] =m match {case Nil => Nilcase x::xs => f(x) ::: flatMap(xs)(f)}

Monad在Scala中很重要,因为它构建了方便的语法来利用Monad结构:

#0 Scala理解

for {i <- 1 to 4j <- 1 to ik <- 1 to j} yield i*j*k

由编译器翻译为:

(1 to 4).flatMap { i =>(1 to i).flatMap { j =>(1 to j).map { k =>i*j*k }}}

键抽象是flatMap,它通过链接绑定计算。
flatMap的每次调用都返回相同的数据结构类型(但值不同),作为链中下一个命令的输入。

在上面的代码片段中,attMap将闭包(SomeType) => List[AnotherType]作为输入并返回List[AnotherType]。需要注意的重要一点是,所有attMap都将相同的闭包类型作为输入并返回相同的类型作为输出。

这就是“绑定”计算线程的原因——for理解中序列的每个项目都必须遵守这个相同的类型约束。


如果您执行两个操作(可能会失败)并将结果传递给第三个操作,例如:

lookupVenue: String => Option[Venue]getLoggedInUser: SessionID => Option[User]reserveTable: (Venue, User) => Option[ConfNo]

但是如果不利用Monad,你会得到令人费解的OOP代码,例如:

val user = getLoggedInUser(session)val confirm =if(!user.isDefined) Noneelse lookupVenue(name) match {case None => Nonecase Some(venue) =>val confno = reserveTable(venue, user.get)if(confno.isDefined)mailTo(confno.get, user.get)confno}

而使用Monad,您可以像所有操作一样使用实际类型(VenueUser),并隐藏Option验证内容,这都是因为for语法的平面图:

val confirm = for {venue <- lookupVenue(name)user <- getLoggedInUser(session)confno <- reserveTable(venue, user)} yield {mailTo(confno, user)confno}

只有在所有三个函数都有Some[X]的情况下才会执行产量部分;任何None都将直接返回到confirm


所以:

单子允许在函数式编程中进行有序计算,这使我们能够以一种很好的结构化形式(有点像DSL)对动作的顺序进行建模。

最大的能力来自于将服务于不同目的的monad组合成应用程序中可扩展的抽象的能力。

monad操作的顺序和线程化由语言编译器完成,该编译器通过闭包的魔力进行转换。


顺便说一句,Monad不仅是FP中使用的计算模型:

范畴理论提出了许多计算模型。其中

  • 计算的箭头模型
  • 计算的Monad模型
  • 计算的应用模型

monad在OO中是否有“自然”解释取决于monad。在Java这样的语言中,你可以将可能monad翻译成检查空指针的语言,这样失败的计算(即在Haskell中产生无)会发出空指针作为结果。你可以将状态monad翻译成通过创建可变变量和改变其状态的方法生成的语言。

monad是内函子范畴中的一个monid。

这个句子组合在一起的信息非常深刻。你在一个monad中使用任何命令式语言。monad是一种“顺序”的领域特定语言。它满足某些有趣的属性,这些属性加在一起使monad成为“命令式编程”的数学模型。Haskell可以轻松定义小型(或大型)命令式语言,它们可以通过多种方式组合。

作为一名面向对象的程序员,你使用你的语言的类层次结构来组织可以在上下文中调用的函数或过程的种类,你称之为对象。monad也是这个想法的抽象,因为不同的monad可以以任意方式组合,有效地将所有sub-monad的方法“导入”到范围中。

在架构上,然后使用类型签名来显式表达哪些上下文可用于计算值。

人们可以为此目的使用monad变压器,并且有一个高质量的所有“标准”monad集合:

  • 列表(非确定性计算,通过将列表视为域)
  • 也许(可能失败,但报告不重要的计算)
  • 错误(可能失败并需要异常处理的计算)
  • Reader(可以由普通Haskell函数组合表示的计算)
  • Writer(具有顺序“渲染”/“日志”的计算(到字符串、html等)
  • 续(续)
  • IO(依赖于底层计算机系统的计算)
  • 状态(上下文包含可修改值的计算)

与相应的monad变压器和类型类。类型类允许通过统一它们的接口来互补组合monad的方法,以便具体的monad可以为monad“种类”实现标准接口。例如,模块Control. Monad. State包含一个类MonadState s m,(State s)是表单的一个实例

instance MonadState s (State s) whereput = ...get = ...

长话短说,monad是一个仿函数,它将“上下文”附加到一个值上,它有一种方法将值注入monad,并且它有一种方法来评估与它附加的上下文相关的值,至少以一种受限制的方式。

所以:

return :: a -> m a

是将a类型的值注入m a类型的monad“action”的函数。

(>>=) :: m a -> (a -> m b) -> m b

是一个接受monad操作,评估其结果并将函数应用于结果的函数。 (>>=) 的好处是结果在同一个monad中。换句话说,在m>>=f中, (>>=) 将结果从m中拉出,并将其绑定到f,因此结果在monad中。(或者,我们可以说 (>>=) 将f拉入m并将其应用于结果。)因此,如果我们有f:: a->m b,并且g:: b->m c,我们可以“排序”操作:

m >>= f >>= g

或者,使用“do符号”

do x <- my <- f xg y

(>>)的类型可能很有启发性

(>>) :: m a -> m b -> m b

它对应于C等过程语言中的(;)运算符。它允许做记号,如:

m = do x <- someQuerysomeAction xtheNextActionandSoOn

在数学和哲学逻辑中,我们有框架和模型,它们“自然”地被单元论建模。解释是一个函数,它研究模型的域,并计算命题(或公式,在推广下)的真值(或推广)。在必然性模态逻辑中,我们可能会说一个命题如果在“所有可能世界”中为真——如果它在所有可接受的域中为真——就是必要的。这意味着命题语言中的模型可以被具体化为一个模型,其域由不同模型的集合组成(一个对应于每个可能世界)。每个monad都有一个名为“连接”的方法,该方法将层展平,这意味着每个结果为monad操作的monad操作都可以嵌入到monad中。

join :: m (m a) -> m a

更重要的是,它意味着monad在“图层堆叠”操作下是封闭的。monad变压器就是这样工作的:它们通过为类似

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

以便我们可以将(MaybeT m)中的操作转换为m中的操作,从而有效地折叠层。在这种情况下,runMaybeT:: MaybeT m a->m(也许a)是我们的类连接方法。(MaybeT m)是一个monad,MaybeT:: m(也许a)->MaybeT m a实际上是m中新型monad操作的构造函数。

仿函数的自由单子是通过堆叠f生成的单子,这意味着f的每个构造函数序列都是自由单子的一个元素(或者更准确地说,与f的构造函数序列树具有相同形状的东西)。自由单子是用最少的样板构造灵活单子的有用技术。在Haskell程序中,我可能会使用自由单子为“高级系统编程”定义简单的单子,以帮助维护类型安全(我只是使用类型及其声明。使用组合器的实现是直接的):

data RandomF r a = GetRandom (r -> a) deriving Functortype Random r a = Free (RandomF r) a

type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.getRandom     :: Random r rrunRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

Monadism是您可以称之为“解释器”或“命令”模式的底层架构,抽象为最清晰的形式,因为每个一元计算都必须“运行”,至少是琐碎的。(运行时系统为我们运行IO monad,是任何Haskell程序的切入点。IO通过按顺序运行IO操作来“驱动”其余的计算)。

连接的类型也是我们得到monad是内函子范畴中的一元半群的声明的地方。由于其类型,连接通常对理论目的更重要。但是理解类型意味着理解monad。从函数组合的意义上说,Join和monad转换器的类连接类型实际上是内函子的组合。用类似Haskell的伪语言来说,

Foo:: m(m a)<->(m. m)a

典型用法中的单子是过程编程异常处理机制的功能等价物。

在现代过程语言中,您将异常处理程序放在一系列语句周围,其中任何一条语句都可能引发异常。如果任何一条语句引发异常,语句序列的正常执行将停止并转移到异常处理程序。

然而,函数式编程语言在哲学上避免了异常处理特性,因为它们具有类似“goto”的性质。函数式编程的观点是函数不应该有像异常这样的“副作用”来扰乱程序流。

实际上,由于I/O的原因,在现实世界中不能排除副作用。函数式编程中的单子用于处理此问题,方法是获取一组链式函数调用(其中任何一个都可能产生意外结果),并将任何意外结果转换为封装数据,这些数据仍然可以安全地流过剩余的函数调用。

控制流程被保留,但意外事件被安全地封装和处理。

从实用的角度来看(总结了许多以前的答案和相关文章中所说的内容),在我看来,monad的基本“目的”(或有用性)之一是利用递归方法调用中隐含的依赖关系,也就是函数组合(即当f1调用f2调用f3时,f3需要在f1之前在f2之前进行评估)以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,顺序组合作为普通序列,例如“f3(); f2(); f1();”在C中-如果您想到f3,f2和f1实际上不返回任何内容的情况,技巧尤其明显[它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列])。

这在涉及副作用时尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,则以什么顺序评估它们并不重要;这是纯函数式语言的一个很好的特性,例如能够并行化这些计算)。

我认为,从这种狭隘的角度来看,monad可以被视为一种语法糖,适用于那些支持懒计算(只在绝对必要时评估事物,遵循一个不依赖于代码呈现的顺序)的语言,并且没有其他方法来表示顺序组合。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数清晰地分离(没有副作用),纯函数可以被懒惰地评估。

这只是一个方面,正如警告这里

我将尝试使用OOP术语做出最短的定义:

泛型类CMonadic<T>是monad,如果它至少定义了以下方法:

class CMonadic<T> {static CMonadic<T> create(T t);  // a.k.a., "return" in Haskellpublic CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell}

如果下列定律适用于所有类型T及其可能值t

左标识:

CMonadic<T>.create(t).flatMap(f) == f(t)

正确的身份

instance.flatMap(CMonadic<T>.create) == instance

结合性:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

示例

一个List monad可能有:

List<int>.create(1) --> [1]

列表[1,2,3]上的platMap可以这样工作:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iterables和Observables也可以变成一元的,Promise和T的也是如此。

评论

单子并没有那么复杂。flatMap函数很像更常见的map。它接收一个函数参数(也称为委托),它可以使用来自泛型类的值调用(立即或稍后,零次或多次)。它期望传递的函数也将其返回值包装在同类泛型类中。为了帮助做到这一点,它提供了create,一个可以从值创建该泛型类实例的构造函数。flatMap的返回结果也是相同类型的泛型类,通常将一个或多个flatMap应用程序的返回结果中包含的相同值打包到之前包含的值中。这允许您根据需要链接flatMap:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10)).flatMap(x => x % 3 == 0? List<string>.create("x = " + x.toString()): List<string>.empty())

碰巧这种泛型类作为大量事物的基本模型是有用的。这(连同范畴理论行话)是Monad看起来如此难以理解或解释的原因。它们是一个非常抽象的东西,只有当它们被专门化时才会变得明显有用。

例如,您可以使用一元容器对异常进行建模。每个容器要么包含操作的结果,要么包含发生的错误。只有当前一个函数在容器中打包了一个值时,才会调用attMap回调链中的下一个函数(委托)。否则,如果错误被打包,错误将继续通过链接容器传播,直到找到一个容器,该容器具有通过名为.orElse()的方法附加的错误处理程序函数(这样的方法将是允许的扩展)

备注:函数式语言允许你编写可以对任何一元泛型类进行操作的函数。为此,必须为monad编写一个泛型接口。我不知道是否可以用C#编写这样的接口,但据我所知是不可能的:

interface IMonad<T> {static IMonad<T> create(T t); // not allowedpublic IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,// because the function must return the same kind of monad, not just any monad}

monad是一个函数数组

(Pst:函数数组只是一个计算)。

实际上,不是真正的数组(一个单元数组中的一个函数),而是将这些函数链接到另一个函数>>=。>>=允许调整函数i的结果以馈送函数i+1,在它们之间执行计算或者,甚至不调用函数i+1。

这里使用的类型是“带上下文的类型”。这是一个带有“标签”的值。被链接的函数必须接受一个“裸值”并返回一个标记结果。>>=的职责之一是从上下文中提取裸值。还有一个函数“返回”,它接受一个裸值并将其与标签放在一起。

一个例子也许。让我们用它来存储一个简单的整数,在上面进行计算。

-- a * bmultiply :: Int -> Int -> Maybe Intmultiply a b = return  (a*b)
-- divideBy 5 100 = 100 / 5divideBy :: Int -> Int -> Maybe IntdivideBy 0 _ = Nothing -- dividing by 0 gives NOTHINGdivideBy denom num = return (quot num denom) -- quotient of num / denom
-- tagged valueval1 = Just 160
-- array of functions feeded with val1array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3
-- array of funcionts created with the do notation-- equals array1 but for the feeded val1array2 :: Int -> Maybe Intarray2 n = dov <- divideBy 2  nv <- multiply 3 vv <- divideBy 4 vv <- multiply 3 vreturn v
-- array of functions,-- the first >>= performs 160 / 0, returning Nothing-- the second >>= has to perform Nothing >>= multiply 3 ....-- and simply returns Nothing without calling multiply 3 ....array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3
main = doprint array1print (array2 160)print array3

为了说明monad是带有辅助操作的函数数组,请考虑等效于上面的例子,只是使用一个真正的函数数组

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions
myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]
-- function for the machinery of executing each function i with the result provided by function i-1runMyMonad :: Maybe Int -> MyMonad -> Maybe IntrunMyMonad val [] = valrunMyMonad Nothing _ = NothingrunMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

它将像这样使用:

print (runMyMonad (Just 160) myArray1)

为什么我们需要Monad?

  1. 我们想编程仅使用函数。(“函数式编程”毕竟-FP)。
  2. 然后,我们有了第一个大问题。这是一个程序:

    f(x) = 2 * x

    g(x,y) = x / y

    我们怎么能说首先要执行的是什么?我们怎么能形成一个有序的函数序列(即一个程序),只使用函数?

    解决方案:组合函数。如果你想先g然后f,就写f(g(x,y))。好的,但是…

  3. 更多问题:一些函数可能会失败(即g(2,0),除以0)。我们有FP无“例外”。我们如何解决它?

    解决方案:让我们允许函数返回两种东西:而不是g : Real,Real -> Real(函数从两个实数变成一个实数),让我们允许g : Real,Real -> Real | Nothing(函数从两个实数变成(实数或无))。

  4. 但是函数应该(更简单)只返回一件事

    解决方案:让我们创建一种要返回的新类型的数据,一个“拳击型”,它可能包含一个真实的或根本没有。因此,我们可以有g : Real,Real -> Maybe Real。好的,但是…

  5. 现在f(g(x,y))会发生什么?f还没有准备好使用Maybe Real。而且,我们不想更改我们可以与g连接的每个函数来使用Maybe Real

    解决方案:让我们有特殊的“连接”/“组合”/“链接”功能。这样,我们就可以在幕后调整一个函数的输出来支持下一个函数。

    在我们的例子中:g >>= f(连接/组合gf)。我们希望>>=获得g的输出,检查它,如果是Nothing,就不要调用f并返回Nothing;或者相反,提取盒装的Real并将其提供给f。(此算法只是g1类型的>>=的实现)。

  6. 出现了许多其他问题,可以使用相同的模式来解决:1.使用“框”来编码/存储不同的含义/值,并具有像g这样的函数来返回这些“盒装值”。2.让作曲家/链接器g >>= f帮助将g的输出连接到f的输入,所以我们根本不必改变f

  7. 可以使用此技术解决的显着问题是:

    • 具有函数序列中的每个函数(“程序”)可以共享的全局状态:解决方案StateMonad

    • 我们不喜欢“不纯函数”:为相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回一个标记/装箱值:IO monad。

绝对的幸福!!!!

看到我的回答到“什么是monad?”

它从一个激励示例开始,通过示例工作,导出monad的示例,并正式定义“monad”。

它假设不了解函数式编程,并使用具有function(argument) := expression语法的伪代码和最简单的表达式。

这个C++程序是伪代码monad的实现。(参考:M是类型构造函数,feed是“绑定”操作,wrap是“返回”操作。)

#include <iostream>#include <string>
template <class A> class M{public:A val;std::string messages;};
template <class A, class B>M<B> feed(M<B> (*f)(A), M<A> x){M<B> m = f(x.val);m.messages = x.messages + m.messages;return m;}
template <class A>M<A> wrap(A x){M<A> m;m.val = x;m.messages = "";return m;}
class T {};class U {};class V {};
M<U> g(V x){M<U> m;m.messages = "called g.\n";return m;}
M<T> f(U x){M<T> m;m.messages = "called f.\n";return m;}
int main(){V x;M<T> m = feed(f, feed(g, wrap(x)));std::cout << m.messages;}

为了尊重快速的读者,我首先从精确的定义开始,继续快速更“简单的英语”解释,然后移动到示例。

这是一个既简洁又精确的定义略有改动:

Amonad(在计算机科学中)是一个地图:

  • 将某些给定编程语言的每个类型X发送到新类型T(X)(称为“T类型-值在X中的计算”);

  • 配备了组成形式的两个函数的规则f:X->T(Y)g:Y->T(Z)到函数g∘f:X->T(Z)

  • 以一种在明显意义上是联想的方式,并且对于一个名为pure_X:X->T(X)的给定单元函数是单位的,被认为是将一个值带到纯粹的计算中,该计算简单地返回该值。

所以简单来说,monad从任何类型#0传递到另一类型#1的规则从两个函数#2和#3(您想组合但不能)传递到新函数#4的规则。然而,在严格的数学意义上,不是组成。我们基本上是“弯曲”函数的组成或重新定义函数的组成方式。

另外,我们需要monad的组合规则来满足“显而易见”的数学公理:

  • 结合性:将fg组合,然后与h组合(从外部)应该与将gh组合,然后与f组合(从内部)相同。
  • 单位属性:将f身份函数在任一侧组合应该产生f

同样,简单地说,我们不能疯狂地重新定义我们的函数组合:

  • 我们首先需要结合性能够在一行中组合多个函数,例如f(g(h(k(x))),而不用担心指定组合函数对的顺序。由于monad规则只规定了如何组合对函数,如果没有该公理,我们需要知道哪个对是先组合的,依此类推。(请注意,这与交换性属性不同,即fg组成的gf组成的g相同,这不是必需的)。
  • 其次,我们需要单位属性,简单来说,恒等式按照我们期望的方式组成,所以只要能提取恒等式,我们就可以安全地重构函数。

简而言之:monad是类型扩展和组合函数的规则,满足两个公理-结合性和单元格属性。

实际上,您希望由负责为您组合函数的语言、编译器或框架为您实现monad。因此,您可以专注于编写函数的逻辑,而不是担心它们的执行是如何实现的。

基本上就是这样,简而言之。


作为一个职业数学家,我倾向于避免把h称为fg的“组合”。因为从数学的角度来看,它不是。把它称为“组合”错误地假设了h是真正的数学组合,但事实并非如此。它甚至不是fg唯一决定的。相反,它是我们的monad新的函数“组合规则”的结果。即使后者存在,也可能与实际的数学组合完全不同!


为了使它不那么枯燥,让我试着用例子来说明它我用小部分注释,所以你可以直接跳到重点。

异常抛出为Monad示例

假设我们要组合两个函数:

f: x -> 1 / xg: y -> 2 * y

但是没有定义f(0),所以抛出了异常e。那么如何定义组合值g(f(0))?当然,再次抛出异常!也许是相同的e。也许是一个新的更新异常e1

这里到底发生了什么?首先,我们需要新的异常值(不同或相同)。你可以叫它们nothingnull或其他什么,但本质保持不变--它们应该是新的值,例如在我们的例子中不应该是number。我宁愿不叫它们null,以避免与null在任何特定语言中的实现方式混淆。同样,我更喜欢避免nothing,因为它经常与null相关联,原则上,这是null应该做的,然而,无论出于什么实际原因,这一原则经常被扭曲。

究竟什么是例外?

对于任何有经验的程序员来说,这都是一件小事,但我想说几句话来消除任何混乱:

异常是一个封装有关无效执行结果如何发生的信息的对象。

这可以包括丢弃任何细节并返回单个全局值(如NaNnull)或生成长日志列表或究竟发生了什么,将其发送到数据库并在分布式数据存储层上复制;)

这两个极端的例外例子之间的重要区别在于,在第一种情况下有没有副作用。在第二种情况下有。这就引出了(千元)问题:

纯函数中是否允许异常?

简短的回答:是的,但只有当它们不会导致副作用时。

更长的答案。为了纯粹,你的函数的输出必须由它的输入唯一确定。因此,我们通过将0发送给我们称为异常的新抽象值e来修改我们的函数f。我们确保值e不包含不受我们的输入(x)唯一确定的外部信息。所以这是一个没有副作用的异常示例:

e = {type: error,message: 'I got error trying to divide 1 by 0'}

这里有一个副作用:

e = {type: error,message: 'Our committee to decide what is 1/0 is currently away'}

实际上,只有当该消息在未来可能发生变化时,它才会产生副作用。但如果保证它永远不会改变,则该值将成为唯一可预测的,因此没有副作用。

一个返回42的函数显然是纯的。但是如果有人疯狂地决定让42成为一个值可能会改变的变量,那么这个函数在新的条件下就不再是纯的了。

请注意,为了简单起见,我使用了对象字面符号来演示本质。不幸的是,在JavaScript等语言中,事情搞砸了,其中error不是我们在函数组合方面想要的行为方式的类型,而像nullNaN这样的实际类型并不是这样的行为,而是通过一些人工的、并不总是直观的类型转换。

类型扩展

由于我们想改变异常中的消息,我们实际上为整个异常对象声明了一个新的类型E,然后这就是maybe number所做的,除了它令人困惑的名称,即类型number或新的异常类型E,所以它实际上是numberE的联合number | E。特别是,它取决于我们想要如何构造E,这既没有建议也没有反映在名称maybe number中。

什么是功能组合?

它是数学运算取函数f: X -> Yg: Y -> Z和构造它们的组成为满足h(x) = g(f(x))的函数h: X -> Z。当结果f(x)不允许作为g的参数时,这个定义就会出现问题。

在数学中,这些函数不能在没有额外工作的情况下组成。对于我们上面的fg示例,严格的数学解决方案是从f的定义集中删除0。有了新的定义集(新的更严格的x类型),f可以与g组合。

但是,像这样限制f的定义集在编程中不是很实际。相反,可以使用异常。

或者作为另一种方法,创建人工值,如NaNundefinednullInfinity等。因此,您评估1/0Infinity1/-0-Infinity。然后将新值强制返回到您的表达式中,而不是抛出异常。导致您可能会或可能不会发现可预测的结果:

1/0                // => InfinityparseInt(Infinity) // => NaNNaN < 0            // => falsefalse + 1          // => 1

我们回到了常规数字,准备继续前进;)

JavaScript允许我们不惜任何代价继续执行数字表达式,而不会像上面的例子中那样抛出错误。这意味着,它还允许组合函数。这正是monad的意义所在——组合满足本答案开头定义的公理的函数是一条规则。

但是,由JavaScript处理数字错误的实现产生的组合函数规则是单子吗?

要回答这个问题,所有你需要的是检查公理(左作为练习,不是问题的一部分;)。

抛出异常可以用来构造monad吗?

事实上,一个更有用的monad将是规定规则如果f为某个x抛出异常,那么它与任何g的组合也是如此。加上使异常E在全球唯一,只有一个可能的值(范畴理论中的终端对象)。现在这两个公理可以立即检查,我们得到了一个非常有用的monad。结果就是众所周知的也许是monad

在OO术语中,monad是一个流畅的容器。

最低要求是class <A> Something的定义,它支持构造函数Something(A a)和至少一个方法Something<B> flatMap(Function<A, Something<B>>)

可以说,如果你的monad类有任何带有签名Something<B> work()的方法保留了类的规则——编译器在编译时烘焙在platMap中,它也会被计算在内。

为什么monad有用?因为它是一个允许保留语义学的链式操作的容器。例如,Optional<?>Optional<String>Optional<Integer>Optional<MyClass>等保留了isPresent的语义学。

举个粗略的例子,

Something<Integer> i = new Something("a").flatMap(doOneThing).flatMap(doAnother).flatMap(toInt)

请注意,我们以字符串开头,以整数结尾。很酷。

在OO中,它可能需要一点手挥舞,但任何返回另一个东西子类的东西上的方法都满足返回原始类型容器的容器函数的标准。

这就是保持语义学的方式——即容器的含义和操作不会改变,它们只是包装和增强容器内的对象。

一个简单的Monads解释与Marvel的案例研究是这里

单子是用于对有效的依赖函数进行排序的抽象。这里的有效意味着它们以F[A]的形式返回一个类型,例如Option[A],其中Option是F,称为类型构造函数。让我们通过2个简单的步骤来看看

  1. 下面的函数组合是传递的。所以要从A到C,我可以组合A=>B和B=>C。
 A => C   =   A => B  andThen  B => C

输入图片描述

  1. 但是,如果函数返回像Option[A]这样的效果类型,即A=>F[B]组合不起作用,我们需要A=>B,但我们有A=>F[B]。
    输入图片描述

    我们需要一个特殊的运算符“bind”,它知道如何融合这些返回F[A]的函数。

 A => F[C]   =   A => F[B]  bind  B => F[C]

"绑定"函数是为特定的F定义的。

对于任何一个,还有“退货”,类型为A=>F[A],也为特定的F定义。要成为Monad,F必须为它定义这两个函数。

因此,我们可以从任何纯函数A=>B构造一个有效函数A=>F[B]

 A => F[B]   =   A => B  andThen  return

但是给定的F也可以定义自己不透明的“内置”特殊函数,这些类型用户无法定义自己(在语言中),例如

  • “随机”(范围=>随机[Int]
  • “print”(String=>IO[()]
  • “尝试……捕获”等。

我在分享我对Monad的理解,这在理论上可能并不完美。Monad是关于上下文传播的。Monad是,你为一些数据(或数据类型)定义一些上下文,然后定义该上下文将如何在整个处理管道中与数据一起携带。定义上下文传播主要是定义如何合并多个上下文(同一类型)。使用Monad也意味着确保这些上下文不会意外地从数据中剥离出来。另一方面,其他无上下文的数据可以被带入新的或现有的上下文中。那么这个简单的概念可以用来确保程序的编译时正确性。

我能想到的最简单的解释是monad是一种使用修饰结果(又名Kleisli修饰)组合函数的方式。“修饰”函数具有签名a -> (b, smth),其中ab是类型(想想IntBool),它们可能彼此不同,但不一定-smth是“上下文”或“修饰”。

这种类型的函数也可以写成a -> m b,其中m等价于“修饰”smth。因此,这些是在上下文中返回值的函数(想想记录其操作的函数,其中smth是记录消息;或者执行输入\输出并且其结果取决于IO操作的结果的函数)。

monad是一个接口(“类型类”),它使实现者告诉它如何组合这些函数。实现者需要为任何想要实现接口的类型m定义一个组合函数(a -> m b) -> (b -> m c) -> (a -> m c)(这是Kleisli组合)。

因此,如果我们有一个元组类型(Int, String)表示Int上的计算结果,也记录了它们的操作,(_, String)是“修饰”-操作的日志-以及两个函数increment :: Int -> (Int, String)twoTimes :: Int -> (Int, String),我们希望获得一个函数incrementThenDouble :: Int -> (Int, String),它是两个函数的组合,也考虑了日志。

在给定的示例中,两个函数的monad实现应用于整数值2incrementThenDouble 2(等于twoTimes (increment 2))将返回(6, " Adding 1. Doubling 3."),中间结果increment 2等于(3, " Adding 1.")twoTimes 3等于(6, " Doubling 3.")

从这个Kleisli合成函数可以导出通常的一元函数。

快速解说:

单子(在函数式编程中)是具有上下文相关行为的函数

上下文作为参数传递,从该monad的前一次调用返回。它使它看起来像是同一个参数在后续调用中产生不同的返回值。

等效:单子是函数,其实际参数依赖于调用链的过去调用。

典型例子:有状态函数。

FAQ

等等,你说的“行为”是什么意思?

行为意味着您为特定输入获得的返回值和副作用。

但是他们有什么特别的?

在过程语义学中:什么都没有。但它们仅使用纯函数建模。这是因为像Haskell这样的纯函数式编程语言只使用本身没有状态的纯函数。

那么,国家从何而来?

状态性来自函数调用执行的顺序性。它允许嵌套函数通过多个函数调用拖动某些参数。这模拟状态。monad只是一种软件模式,用于将这些附加参数隐藏在闪亮函数的返回值后面,通常称为returnbind

为什么Haskell中的输入/输出是monad?

因为显示的文本是操作系统中的一个状态。如果您多次读取或写入相同的文本,每次调用后操作系统的状态将不相等。相反,您的输出设备将显示3倍的文本输出。为了对操作系统做出正确的反应,Haskell需要将操作系统状态建模为monad。

从技术上讲,您不需要monad定义。纯函数式语言可以出于同样的目的使用“唯一性类型”的想法。

非函数式语言中是否存在monad?

是的,解释器基本上是一个复杂的monad,解释每个指令并将其映射到操作系统中的新状态。

详细解释:

monad(在函数式编程中)是纯功能软件模式。monad是可以执行纯函数调用链自动维护环境(对象)。函数结果修改该环境或与该环境交互。

换句话说,monad是一个“函数中继器”或“函数链”,它链接和评估自动维护的环境中的参数值。通常链接的参数值是“更新函数”,但实际上可以是任何对象(带有方法或组成容器的容器元素)。monad是在每个评估参数之前和之后执行的“粘合代码”。这个粘合代码函数“bind”应该将每个参数的环境输出集成到原始环境中。

因此,monad以特定于特定monad的实现方式连接所有参数的结果。参数之间的控制和数据是否或如何流动也是特定于实现的。

这种相互交织的执行允许建模完整的命令式控制流(如在GOTO程序中)或仅使用纯函数的并行执行,但也允许在函数调用之间进行副作用、临时状态或异常处理,即使应用的函数不知道外部环境。

编辑:请注意,monad可以在任何类型的控制流图中评估功能链,甚至是非确定性的NFA类方式,因为剩余的链是延迟评估的,并且可以在链的每个点进行多次评估,这允许在链中回溯。

使用monad概念的原因是纯函数范式,它需要一个工具来以纯粹的方式模拟典型的非纯粹建模行为,而不是因为它们做了一些特殊的事情。

面向OOP的Monads

在OOP中,monad是一个典型的对象

  • 通常称为return构造函数,它将值转换为环境的初始实例

  • 一个可链接的参数应用法,通常称为bind,它使用作为参数传递的函数的返回环境来维护对象的状态。

有些人还提到了第三个函数join,它是bind的一部分。因为“参数函数”被环境评估,它们的结果嵌套在环境本身中。join是“取消嵌套”结果(扁平化环境)的最后一步,以用新环境替换环境。

monad可以实现Builder模式,但允许更广泛的使用。

示例(Python)

我认为monad最直观的例子是Python中的关系运算符:

result =  0 <= x == y < 3

您会看到它是一个monad,因为它必须携带一些单个关系运算符调用不知道的布尔状态。

如果您考虑如何在低级别上实现它而不会出现短路行为,那么您将获得一个monad实现:

# result = ret(0)result = (0, true)# result = result.bind(lambda v: (x, v <= x))result[1] = result[1] and result[0] <= xresult[0] = x# result = result.bind(lambda v: (y, v == y))result[1] = result[1] and result[0] == yresult[0] = y# result = result.bind(lambda v: (3, v < 3))result[1] = result[1] and result[0] < 3result[0] = 3result = result[1]      # not explicit part of a monad

一个真正的monad最多计算一次每个参数。

现在想想“结果”变量,你会得到这个链:

ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)

optional/maybe是最基本的一元类型

Monad是关于函数组合的。如果您有函数f:optional<A>->optional<B>g:optional<B>->optional<C>h:optional<C>->optional<D>。那么您可以组合它们

optional<A> opt;h(g(f(opt)));

monad类型的好处是您可以组合f:A->optional<B>g:B->optional<C>h:C->optional<D>。他们可以这样做,因为monadic接口提供了绑定运算符

auto optional<A>::bind(A->optional<B>)->optional<B>

作文可以写

optional<A> optopt.bind(f).bind(g).bind(h)

monad的好处是我们不再需要在每个f,g,h中处理if(!opt) return nullopt;的逻辑,因为这个逻辑被移动到绑定运算符中。

ranges/lists/iterables是第二个最基本的monad类型。

ranges的一元特征是我们可以变换然后变平,即从编码为整数范围[36, 98]的句子开始

我们可以转换为[['m','a','c','h','i','n','e',' '], ['l','e','a','r','n','i','n','g', '.']]

然后把['m','a','c','h','i','n','e', ' ', 'l','e','a','r','n','i','n','g','.']压扁

而不是写这个代码

vector<string> lookup_table;auto stringify(vector<unsigned> rng) -> vector<char>{vector<char> result;for(unsigned key : rng)for(char ch : lookup_table[key])result.push_back(ch);result.push_back(' ')result.push_back('.')return result}

我们可以写这个

auto f(unsigned key) -> vector<char>{vector<char> result;for(ch : lookup_table[key])result.push_back(ch);return result}auto stringify(vector<unsigned> rng) -> vector<char>{return rng.bind(f);}

monad将for循环for(unsigned key : rng)向上推到bind函数中,理论上允许更容易推理的代码。毕达哥拉斯三元组可以在range-v3中生成嵌套绑定(而不是我们在optional中看到的链式绑定)

auto triples =for_each(ints(1), [](int z) {return for_each(ints(1, z), [=](int x) {return for_each(ints(x, z), [=](int y) {return yield_if(x*x + y*y == z*z, std::make_tuple(x, y, z));});});});