在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?

我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。


当前回答

Monad用于控制流,就像抽象数据类型用于数据一样。

换句话说,许多开发人员对集合、列表、字典(或哈希、或地图)和树的概念很熟悉。在这些数据类型中有许多特殊情况(例如InsertionOrderPreservingIdentityHashMap)。

然而,当面对程序“流”时,许多开发人员还没有接触到比if、switch/case、do、while、goto(grr)和(可能)闭包更多的构造。

因此,monad只是一个控制流构造。替代monad的更好短语是“控制类型”。

因此,monad具有用于控制逻辑、语句或函数的槽——数据结构中的等价物是,某些数据结构允许您添加数据,并删除数据。

例如,“if”monad:

if( clause ) then block

最简单的是有两个槽:一个子句和一个块。if monad通常用于评估子句的结果,如果不是false,则评估块。许多开发人员在学习“如果”时并没有接触到monad,而且编写有效的逻辑并不需要理解monad。

monad可能会变得更复杂,就像数据结构可能变得更复杂一样,但monad有很多大类可能具有相似的语义,但实现和语法不同。

当然,数据结构可以在单子上迭代或遍历,也可以以同样的方式进行评估。

编译器可能支持也可能不支持用户定义的monad。哈斯克尔当然知道。Ioke有一些类似的功能,尽管语言中没有使用monad一词。

其他回答

遵循您简短、简洁、实用的指示:

理解monad最简单的方法是在上下文中应用/组合函数。假设你有两个计算,它们都可以看作是两个数学函数f和g。

f取一个String并生成另一个String(取前两个字母)g获取一个String并生成另一个String(大写转换)

因此,在任何语言中,“取前两个字母并将其转换为大写”的转换都会写成g(f(“某个字符串”))。因此,在纯完美函数的世界中,合成只是:先做一件事,然后再做另一件事。

但假设我们生活在一个功能可能失败的世界中。例如:输入字符串可能有一个字符长,因此f将失败。所以在这种情况下

f获取一个String并生成一个String或Nothing。g仅在f未失败时生成字符串。否则,将不生成任何内容

所以现在,g(f(“somestring”))需要一些额外的检查:“计算f,如果它失败,那么g应该返回Nothing,否则计算g”

此思想可应用于任何参数化类型,如下所示:

让Context[Sometype]是Context中Sometype的计算。考虑功能

f: :AnyType->上下文[Sometype]g: :某些类型->上下文[AnyOtherType]

合成g(f())应该读作“compute f。在这个上下文中,做一些额外的计算,然后计算g,如果它在上下文中有意义”

我对monads还是个新手,但我想我会分享一个我觉得读起来很好的链接(带图片!!):http://www.matusiak.eu/numerodix/blog/2012/3/11/monads-for-the-layman/(无隶属关系)

基本上,我从这篇文章中得到的温暖而模糊的概念是monad基本上是适配器,允许不同的函数以可组合的方式工作,即能够将多个函数串起来并混合和匹配它们,而不用担心不一致的返回类型等。因此,当我们尝试制作这些适配器时,BIND函数负责将苹果与苹果、橙子与橙子放在一起。LIFT功能负责使用“较低级别”的功能,并将其“升级”为与BIND功能一起使用并可组合。

我希望我做得对,更重要的是,希望这篇文章对单子有一个有效的观点。如果没有别的话,这篇文章有助于激发我学习更多关于单子叶植物的欲望。

在Scala的上下文中,您会发现以下是最简单的定义。基本上,flatMap(或bind)是“关联”的,并且存在一个标识。

trait M[+A] {
  def flatMap[B](f: A => M[B]): M[B] // AKA bind

  // Pseudo Meta Code
  def isValidMonad: Boolean = {
    // for every parameter the following holds
    def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
      x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))

    // for every parameter X and x, there exists an id
    // such that the following holds
    def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
      x.flatMap(id) == x
  }
}

E.g.

// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)

// Observe these are identical. Since Option is a Monad 
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)

scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)


// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)

// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)

scala> Some(7)
res214: Some[Int] = Some(7)

注:严格地说,函数编程中的Monad的定义与范畴理论中的Monard的定义不同,后者是按映射和展平的顺序定义的。尽管它们在某些映射下是等价的。这个演示非常好:http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category

另一种尝试是解释monad,只使用Python列表和map函数。我完全接受这不是一个完整的解释,但我希望它能触及核心概念。

我从Monads上的funfunfunction视频和Learn You A Haskell章节“为了几个Monads更多”中得到了这一点的基础。我强烈推荐观看funfunfunction视频。

最简单的是,Monad是具有map和flatMap函数(在Haskell中绑定)的对象。有一些额外的必需财产,但这些是核心属性。

flatMap“展平”map的输出,对于列表,这只是连接列表的值,例如。

concat([[1], [4], [9]]) = [1, 4, 9]

因此,在Python中,我们基本上可以通过以下两个函数实现Monad:

def flatMap(func, lst):
    return concat(map(func, lst))

def concat(lst):
    return sum(lst, [])

func是任何接受值并返回列表的函数。

lambda x: [x*x]

解释

为了清楚起见,我通过一个简单的函数在Python中创建了concat函数,该函数将列表相加,即[]+[1]+[4]+[9]=[1,4,9](Haskell有一个原生的concat方法)。

我假设你知道地图功能是什么,例如:

>>> list(map(lambda x: [x*x], [1,2,3]))
[[1], [4], [9]]

展平是Monad的关键概念,对于每个作为Monad的对象,这种展平允许您获得Monad中包裹的值。

现在我们可以呼叫:

>>> flatMap(lambda x: [x*x], [1,2,3])
[1, 4, 9]

这个lambda取一个值x并将其放入一个列表中。monad适用于从值到monad类型的任何函数,所以在本例中是列表。

这是你的monad定义。

我认为为什么它们有用的问题已经在其他问题中得到了回答。

更多说明

其他不是列表的例子有JavaScript Promise,它有then方法,JavaScript Streams有flatMap方法。

因此Promise和Streams使用了一个稍微不同的函数,它将Stream或Promise展平,并从内部返回值。

Haskell列表monad具有以下定义:

instance Monad [] where  
    return x = [x]  
    xs >>= f = concat (map f xs)  
    fail _ = [] 

即有三个函数return(不要与大多数其他语言中的return混淆)、>>=(flatMap)和fail。

希望您能看到以下两者之间的相似之处:

xs >>= f = concat (map f xs)

and:

def flatMap(f, xs):
    return concat(map(f, xs))

第一:如果你不是数学家,monad这个词有点空洞。另一个术语是计算构建器,它更能描述它们的实际用途。

它们是链接操作的模式。它看起来有点像面向对象语言中的方法链接,但机制略有不同。

该模式主要用于函数式语言(特别是Haskell,它普遍使用monad),但也可以用于支持高阶函数的任何语言(即可以将其他函数作为参数的函数)。

JavaScript中的数组支持该模式,因此让我们将其作为第一个示例。

模式的要点是我们有一个类型(在本例中为Array),它有一个以函数作为参数的方法。提供的操作必须返回相同类型的实例(即返回数组)。

首先是一个不使用monad模式的方法链接示例:

[1,2,3].map(x => x + 1)

结果是[2,3,4]。代码不符合monad模式,因为我们作为参数提供的函数返回的是数字,而不是数组。monad形式的相同逻辑是:

[1,2,3].flatMap(x => [x + 1])

这里我们提供了一个返回Array的操作,所以现在它符合模式。flatMap方法为数组中的每个元素执行提供的函数。它期望每个调用都有一个数组作为结果(而不是单个值),但将得到的数组集合并为一个数组。所以最终的结果是相同的,数组[2,3,4]。

(提供给map或flatMap等方法的函数参数在JavaScript中通常称为“回调”。我将其称为“操作”,因为它更通用。)

如果我们连锁多个操作(以传统方式):

[1,2,3].map(a => a + 1).filter(b => b != 3)

数组中的结果[2,4]

monad形式的相同链接:

[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])

产生相同的结果,即数组[2,4]。

您将立即注意到monad格式比非monad格式更难看!这正好表明单子不一定“好”。它们是一种有时有益有时不有益的模式。

请注意,monad模式可以以不同的方式组合:

[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))

这里的绑定是嵌套的,而不是链式的,但结果是一样的。这是单子的一个重要属性,我们稍后会看到。这意味着组合的两个操作可以被视为单个操作。

该操作允许返回具有不同元素类型的数组,例如,将数字数组转换为字符串数组或其他东西;只要它仍然是一个数组。

这可以使用Typescript表示法更正式地描述。数组的类型为array<T>,其中T是数组中元素的类型。flatMap()方法接受类型为T=>Array<U>的函数参数,并返回一个Array<U>。

一般来说,monad是任何类型的Foo<Bar>,它有一个“bind”方法,该方法接受类型为Bar=>Foo<Baz>的函数参数,并返回一个Foo<Baz>。

这回答了单子是什么。这个答案的其余部分将试图通过示例来解释为什么monads在Haskell这样的语言中是一种有用的模式,而Haskell对monads有很好的支持。

Haskell和Do表示法

要将map/filter示例直接转换为Haskell,我们将flatMap替换为>>=运算符:

[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b] 

>>=运算符是Haskell中的绑定函数。当操作数是一个列表时,它与JavaScript中的flatMap相同,但对于其他类型,它被重载了不同的含义。

但是Haskell还为monad表达式提供了专用语法do块,它完全隐藏了绑定运算符:

 do a <- [1,2,3] 
    b <- [a+1] 
    if b == 3 then [] else [b] 

这将隐藏“管道”,并让您专注于在每个步骤中应用的实际操作。

在do块中,每一行都是一个操作。约束仍然认为块中的所有操作都必须返回相同的类型。因为第一个表达式是一个列表,所以其他操作也必须返回一个列表。

向后箭头<-看起来像赋值,但请注意,这是绑定中传递的参数。因此,当右侧的表达式是整数列表时,左侧的变量将是一个整数,但将对列表中的每个整数执行。

示例:安全导航(Maybe类型)

关于列表,让我们来看看monad模式如何对其他类型有用。

某些函数可能不总是返回有效值。在Haskell中,这由Maybe类型表示,该类型是Just value或Nothing选项。

总是返回有效值的链接操作当然很简单:

streetName = getStreetName (getAddress (getUser 17)) 

但如果任何函数都可以返回Nothing呢?我们需要单独检查每个结果,如果不是Nothing,则只将值传递给下一个函数:

case getUser 17 of
      Nothing -> Nothing 
      Just user ->
         case getAddress user of
            Nothing -> Nothing 
            Just address ->
              getStreetName address

很多重复检查!想象一下如果链条更长。Haskell用Maybe的monad模式解决了这个问题:

do
  user <- getUser 17
  addr <- getAddress user
  getStreetName addr

这个do块调用Maybe类型的绑定函数(因为第一个表达式的结果是Maybe)。绑定函数仅在值为Just值时执行以下操作,否则只传递Nothing。

这里使用monad模式来避免重复代码。这与其他一些语言使用宏来简化语法的方式类似,尽管宏以非常不同的方式实现了相同的目标。

请注意,Haskell中monad模式和monad友好语法的结合导致了代码更干净。在JavaScript这样的语言中,如果没有对monad的任何特殊语法支持,我怀疑monad模式是否能够在这种情况下简化代码。

可变状态

Haskell不支持可变状态。所有变量都是常量,所有值都是不可变的。但State类型可用于模拟具有可变状态的编程:

add2 :: State Integer Integer
add2 = do
        -- add 1 to state
         x <- get
         put (x + 1)
         -- increment in another way
         modify (+1)
         -- return state
         get


evalState add2 7
=> 9

add2函数构建一个monad链,然后以7作为初始状态对其求值。

显然,这在Haskell中才有意义。其他语言支持开箱即用的可变状态。Haskell通常在语言特性上是“选择加入”的——您可以在需要时启用可变状态,并且类型系统确保效果是显式的。IO是这方面的另一个例子。

IO

IO类型用于链接和执行“不纯”函数。

与任何其他实用语言一样,Haskell有一系列与外界接口的内置函数:putStrLine、readLine等。这些函数被称为“不纯”,因为它们要么会产生副作用,要么会产生不确定性的结果。即使是像获取时间这样简单的事情也被认为是不纯洁的,因为结果是不确定的——用相同的参数调用两次可能会返回不同的值。

纯函数是确定性的——它的结果完全取决于传递的参数,除了返回值之外,它对环境没有任何副作用。

Haskell大力鼓励使用纯函数——这是该语言的一个主要卖点。不幸的是,对于纯粹主义者来说,你需要一些不纯的函数来做任何有用的事情。Haskell折衷方案是将纯函数和不纯函数彻底分开,并保证纯函数无法直接或间接执行不纯函数。

这是通过给所有不纯函数赋予IO类型来保证的。Haskell程序的入口点是具有IO类型的主函数,因此我们可以在顶层执行不纯的函数。

但是该语言如何防止纯函数执行不纯函数?这是因为Haskell的懒惰本性。只有当某个函数的输出被其他函数消耗时,才执行该函数。但除了将IO值分配给main之外,没有办法使用它。因此,如果一个函数想要执行一个不纯的函数,它必须连接到main并具有IO类型。

对IO操作使用monad链接还可以确保它们以线性和可预测的顺序执行,就像命令式语言中的语句一样。

这让我们看到大多数人会用Haskell编写的第一个程序:

main :: IO ()
main = do 
        putStrLn ”Hello World”

当只有一个操作,因此没有什么要绑定时,do关键字是多余的,但为了保持一致性,我还是保留了它。

()类型表示“无效”。这种特殊的返回类型仅适用于因其副作用而调用的IO函数。

更长的示例:

main = do
    putStrLn "What is your name?"
    name <- getLine
    putStrLn "hello" ++ name

这构建了一个IO操作链,因为它们被分配给主功能,所以它们被执行。

将IO与Maybe进行比较表明了monad模式的多功能性。对于Maybe,该模式用于通过将条件逻辑移动到绑定函数来避免重复代码。对于IO,该模式用于确保IO类型的所有操作都是有序的,并且IO操作不会“泄漏”到纯函数。

总结

在我的主观看法中,monad模式只有在对该模式有一些内置支持的语言中才真正有价值。否则,它只会导致过于复杂的代码。但是Haskell(和其他一些语言)有一些内置支持,隐藏了繁琐的部分,然后该模式可以用于各种有用的事情。喜欢:

避免重复代码(可能)为程序的分隔区域添加可变状态或异常等语言特性。将讨厌的东西与美好的东西隔离开来(IO)嵌入式域特定语言(解析器)将GOTO添加到语言中。