就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?

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

使现代化

为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?


当前回答

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

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

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public 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))

示例:

列表monad可能具有:

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

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

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

Iterables和Observables也可以是monadic,以及Promise和Task。

评论:

修道院没有那么复杂。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())

恰好这种泛型类作为大量事物的基础模型非常有用。这(加上范畴理论的对立)是莫纳斯看起来如此难以理解或解释的原因。它们是一个非常抽象的东西,只有在它们被专门化之后才会变得明显有用。

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

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

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public 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的一个基本“目的”(或有用性)是利用递归方法调用(即函数组合)中隐含的依赖关系(即,当f1调用f2调用f3时,f3需要在f1之前的f2之前求值),以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,作为一个普通序列的顺序合成,例如C中的“f3();f2();f1();”),如果你想到f3、f2和f1实际上什么都不返回的情况(它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列),那么这个技巧就特别明显了。

当涉及到副作用时,这一点尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,那么它们的求值顺序无关紧要;这是纯函数语言的一个很好的特性,例如能够并行化这些计算)。函数越纯越好。

我认为,从这个狭隘的角度来看,monad可以被视为支持惰性求值的语言的语法糖(只有在绝对必要时才求值,遵循不依赖于代码表示的顺序),并且没有其他表示顺序合成的方法。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数(没有副作用)完全分离,纯函数可以延迟求值。

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

典型用法中的Monad在功能上等同于过程编程的异常处理机制。

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

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

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

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

monad是一个函数数组

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

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

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

Maybe的一个例子。让我们使用它来存储一个简单的整数,以便进行计算。

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return 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 = do
     print array1
     print (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-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

它的用法如下:

print (runMyMonad (Just 160) myArray1)

这里有一个简单的Monads解释和Marvel的案例研究。

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

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

 A => C   =   A => B  andThen  B => C

然而,如果函数返回一个像Option[A]这样的效果类型,即A=>F[B],则合成不起作用,因为我们需要A=>B,但我们有A=>F[B]。我们需要一个特殊的运算符“bind”,它知道如何融合这些返回F[a]的函数。

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

“bind”函数是为特定的F定义的。

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

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

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

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

“随机”(范围=>随机[Int])“print”(字符串=>IO[()])“尝试…捕捉”等。

我能想到的最简单的解释是,单声道是一种用符号化结果组成函数的方式(也称为克莱斯利合成)。“embelished”函数具有签名a->(b,smth),其中a和b是可能彼此不同但不一定不同的类型(想想Int,Bool),smth是“上下文”或“embelisement”。

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

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

所以,如果我们说我们有一个元组类型(Int,String),它表示Int上的计算结果,(_,String)是“embelisation”-动作的日志-和两个函数increment::Int->(Int,String)和twoTimes::Int->(Int、String),我们希望获得一个函数incamentThenDouble::Int->(Int),这是两个函数的组合,也考虑了日志。

在给定的示例中,两个函数的monad实现应用于整数值2增量ThenDouble 2(等于2倍(增量2))将返回(6,“加法1”。中间结果的增量2等于(3,“加1”),2乘以3等于(6,“加3”)

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