天天看点

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

用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?

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

编辑:

为了阐明我正在寻找的理解类型,假设您正在将具有monad的FP应用程序转换为OOP应用程序。 您将如何将monad的责任移植到OOP应用程序?

#1楼

monad在OO中是否具有“自然”的解释取决于monad。 在Java之类的语言中,您可以将monad转换为检查空指针的语言,以使失败的计算(即,在Haskell中不产生任何结果)发出空指针作为结果。 您可以将状态monad转换为通过创建可变变量和更改其状态的方法生成的语言。

一元论是endofunctors类别中的一个monoid。

句子汇总的信息非常深刻。 您可以使用任何命令式语言在monad中工作。 monad是一种“排序的”领域特定语言。 它满足了某些有趣的特性,这些特性使monad成为“命令式编程”的数学模型。 Haskell使定义小型(或大型)命令式语言变得容易,这些语言可以通过多种方式组合。

作为OO程序员,您使用语言的类层次结构来组织可以在上下文中调用的函数或过程,即对象。 只要可以以任意方式组合不同的monad,将monad的所有方法有效地“导入”到范围内,monad也是该想法的抽象。

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

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

  • 列表(非确定性计算,通过将列表视为域)
  • 可能(计算可能会失败,但报告不重要)
  • 错误(可能失败并需要异常处理的计算
  • 阅读器(可以由简单的Haskell函数的组合表示的计算)
  • 编写器(具有顺序“呈现” /“记录”(到字符串,html等)的计算
  • 续(续)
  • IO(取决于基础计算机系统的计算)
  • 状态(上下文中包含可修改值的计算)

与相应的monad转换器和类型类。 类型类允许一种补充方法,通过统一单子接口来组合单子,以便具体的单子可以为单子“种类”实现标准接口。 例如,模块Control.Monad.State包含类MonadState sm,而(State)是以下形式的实例

instance MonadState s (State s) where
    put = ...
    get = ...
           

长话大说,单子是一个函子,它把“上下文”附加到值上,它有一种向单子中注入一个值的方式,并且至少有一种方法可以根据附加到它的上下文来评估值。以受限的方式。

所以:

return :: a -> m a
           

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

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

是执行monad操作,评估其结果并将功能应用于结果的函数。 关于(>> =)的一件整洁的事情是结果在同一单子中。 换句话说,在m >> = f中,(>> =)将结果从m中拉出,并将其绑定到f,这样结果就在单子中。 (或者,我们可以说(>> =)将f拉入m并将其应用于结果。)因此,如果我们有f :: a-> mb和g :: b-> mc,我们可以“序列”动作:

m >>= f >>= g
           

或者,使用“做记号”

do x <- m
   y <- f x
   g y
           

(>>)的类型可能是发光的。 它是

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

它对应于过程语言(如C)中的(;)运算符。它允许使用do表示法,例如:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn
           

在数学和哲学逻辑中,我们有框架和模型,这些框架和模型是用一元论“自然”建模的。 解释是一种函数,它研究模型的域并计算命题(或公式,在归纳法下)的真值(或归纳)。 在必要性的模态逻辑中,我们可以说,如果一个命题在“每个可能的世界”中都是正确的,则该命题是必要的-如果在每个允许的领域中都是正确的。 这意味着可以将命题语言中的模型重新定义为一个模型,该模型的域包括不同模型的集合(每个模型对应于每个可能的世界)。 每个monad都有一个名为“ join”的方法,它可以使层变平,这意味着结果为monad动作的每个monad动作都可以嵌入到monad中。

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

更重要的是,这意味着monad在“ layer stacking”操作下是关闭的。 这就是monad转换器的工作方式:它们通过为类似类型的类型提供“ join-like”方法来组合monad

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

这样我们就可以将(MaybeT m)中的动作转换为m中的动作,有效地折叠图层。 在这种情况下,runMaybeT :: MaybeT ma-> m(Maybe a)是我们类似join的方法。 (MaybeT m)是monad,而MaybeT :: m(Maybe a)-> MaybeT ma实际上是m中新型monad动作的构造函数。

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

data RandomF r a = GetRandom (r -> a) deriving Functor
type 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 r
runRandomIO   :: 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是您可能称为“解释器”或“命令”模式的基础体系结构,抽象为最清晰的形式,因为每个monadic计算都必须“运行”,至少是微不足道的。 (运行时系统为我们运行IO monad,并且是任何Haskell程序的入口点。IO通过依次运行IO操作来“驱动”其余计算。)

join的类型也是我们在其中获得的声明,即monad是endofunctors类别中的一个monoid。 由于其类型,联接通常对于理论目的更为重要。 但是了解类型意味着了解单子。 从功能组成的角度来看,Join和monad变换器的join-like类型实际上是endofunctors的组成。 放在类似Haskell的伪语言中,

Foo :: m(ma)<->(m。m)一个

#2楼

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

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

但是,功能性编程语言从哲学上避免了由于异常性质而引起的“ goto”异常处理功能。 从功能编程的角度来看,功能不应具有“副作用”,例如破坏程序流程的异常。

实际上,主要由于I / O不能排除现实世界中的副作用。 函数编程中的Monad通过处理一组链接的函数调用(其中任何一个都可能产生意外的结果)并将任何意外的结果转换为封装的数据,从而仍然可以安全地流经其余的函数调用来处理此问题。

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

#3楼

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

当涉及到副作用时,这尤其相关,例如,当某些状态发生变化时(如果f1,f2,f3没有副作用,则以什么顺序评估它们都无关紧要;这是pure的一个重要特性功能语言,以便能够并行化这些计算)。 纯功能越多越好。

我认为从那种狭narrow的角度来看,对于那些倾向于惰性评估(仅在绝对必要时才进行评估,并且遵循不依赖于代码表示的顺序进行评估的语言)的语言,monad可以看作是语法糖。表示顺序组成的其他方式。 最终结果是,可以以强制性的方式自然地呈现“不纯”(即确实具有副作用)的代码部分,而将它们与纯函数(没有副作用)清晰地分开。懒洋洋地评价。

这仅仅是一个方面,虽然,作为警告这里 。

#4楼

我将尝试使用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))
           

例子 :

List 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以及Promises和Tasks也可以设置为monadic。

评论 :

Monad并不那么复杂。

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()

方法连接的错误处理程序函数(此类方法是允许的扩展)

注意 :函数语言允许您编写可以对任何类型的monadic通用类进行操作的函数。 为此,必须编写用于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
}
           

#5楼

单子是一组功能

(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)
           

#6楼

我写了一篇简短的文章,比较标准的OOP python代码和monadic python代码,用图表演示了底层的计算过程。 它假设没有先前的FP知识。 希望你觉得它很有用 - http://nikgrozev.com/2013/12/10/monads-in-15-minutes/

#7楼

来自维基百科 :

在函数式编程中,monad是一种用于表示计算(而不是域模型中的数据)的抽象数据类型。 Monad使程序员可以将动作链接在一起以构建管道,其中每个动作都装饰有monad提供的其他处理规则。 以功能风格编写的程序可以使用monad来构建包括顺序操作1 [2]的过程,或者定义任意控制流(如处理并发,连续或异常)。

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

程序员将编写单子函数来定义数据处理管道。 monad充​​当框架,因为它是一种可重用的行为,它决定调用管道中特定monadic函数的顺序,并管理计算所需的所有秘密工作。[3] 在每个monadic函数返回控制后,将执行流水线中交错的bind和return运算符,并将照顾monad处理的特定方面。

我相信它可以很好地解释它。

#8楼

更新:这个问题是一个非常长的博客系列的主题,您可以在Monads上阅读它 -感谢您提出的伟大问题!

用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?

monad是一种类型的“放大器”,它遵循某些规则并提供某些操作 。

首先,什么是“类型放大器”? 我的意思是说,有一个系统可以让您选择一种类型并将其转换为更特殊的类型。 例如,在C#中考虑

Nullable<T>

。 这是一种放大器。 它允许您采用一个类型,例如

int

,并为该类型添加新功能,即现在可以在以前无法使用的情况下将其设置为null。

作为第二个示例,请考虑

IEnumerable<T>

。 它是一种类型的放大器。 它允许您采用一种类型,例如

string

,并为该类型添加新功能,即,您现在可以从任意数量的单个字符串中构成一个字符串序列。

什么是“某些规则”? 简而言之,存在一种合理的方式,使基础类型上的功能在放大类型上起作用,从而使它们遵循功能组成的正常规则。 例如,如果您有一个整数函数,请说

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

那么

Nullable<int>

上的相应函数可以使所有运算符和其中的调用“像以前一样”一起工作。

(这是非常模糊和不精确的;您要求提供一种解释,该解释没有假定任何有关功能组成的知识。)

什么是“操作”?

  1. 有一个“单位”操作(有时也称为“返回”操作),该操作从普通类型获取值并创建等效的单价值。 本质上,这提供了一种获取未放大类型的值并将其转换为放大类型的值的方法。 可以将其实现为OO语言的构造函数。
  2. 有一个“绑定”操作,它接受一个单子值和一个可以转换该值并返回新单子值的函数。 绑定是定义monad语义的关键操作。 它使我们可以将未放大类型的操作转换为对放大类型的操作,这要遵循前面提到的功能组成规则。
  3. 通常有一种方法可以使未放大类型从放大类型中退回。 严格来说,此操作不需要具有monad。 (尽管如果您想拥有自己的名字是很有必要的。我们将不在本文中进一步讨论。)

同样,以

Nullable<T>

为例。 您可以使用构造函数将

int

转换为

Nullable<int>

。 C#编译器会为您处理大多数可为空的“提升”,但是如果没有,提升转换将非常简单:例如,

int M(int x) { whatever }
           

变成

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

Nullable<int>

int

是通过

Value

属性完成的。

函数转换是关键。 请注意如何可空操作的实际语义-即在一个操作

null

传播的

null

-在转型抓获。 我们可以对此进行概括。

假设您有一个从

int

int

的函数,就像我们原来的

M

。 您可以轻松地使它成为一个接受

int

并返回

Nullable<int>

的函数,因为您可以通过可为null的构造函数运行结果。 现在假设您具有以下高阶方法:

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

看到你能做什么? 现在,任何采用

int

并返回

int

或采用

int

并返回

Nullable<int>

都可以对其应用可为null的语义 。

此外:假设您有两种方法

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

而您想组成它们:

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

也就是说,

Z

X

Y

。 但是您不能这样做,因为

X

接受一个

int

,而

Y

返回一个

Nullable<int>

。 但是,由于您具有“绑定”操作,因此可以进行以下工作:

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

对单声道的绑定操作使放大类型上的功能组合起作用。 我上面挥挥手的“规则”是单子保留正常功能组成的规则。 与标识函数组成的结果将产生原始函数,该组成具有关联性,依此类推。

在C#中,“绑定”称为“ SelectMany”。 看一下它如何在序列monad上工作。 我们需要做两件事:将一个值转换为一个序列,然后对序列进行绑定操作。 作为奖励,我们还具有“将序列变回值”的功能。 这些操作是:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(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真正含义的方法 。

我们可以做得更好。 假设您有一个整数序列,以及一个采用整数并产生字符串序列的方法。 我们可以对绑定操作进行一般化,以允许接受和返回不同放大类型的函数的组合,只要其中一个的输入与另一个的输出匹配即可:

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;            
}
           

因此,现在我们可以说:“将这组单个整数放大为整数序列。将该特定整数转换为一串字符串,放大为一系列字符串。现在将这两个操作放在一起:将这组整数放大为”所有的字符串序列。” Monads使您可以构成放大。

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

这就好比问“单例模式能解决什么问题?”,但我会给它一个机会。

Monad通常用于解决以下问题:

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

C#在其设计中使用了monad。 如前所述,可为空的模式与“也许是单子”非常相似。 LINQ完全由monad组成。

SelectMany

方法是操作组合的语义工作。 (Erik Meijer喜欢指出每个LINQ函数实际上都可以由

SelectMany

实现;其他一切只是为了方便。)

为了阐明我正在寻找的理解类型,假设您正在将具有monad的FP应用程序转换为OOP应用程序。 您将如何将Monad的责任移植到OOP应用程序中?

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

一个很好的起点是我们如何在C#中实现LINQ。 研究

SelectMany

方法; 这是理解序列monad如何在C#中工作的关键。 这是一种非常简单的方法,但功能非常强大!

建议进一步阅读:

  1. 有关C#中monad的更深入和理论上合理的解释,我强烈建议我( Eric Lippert的同事)Wes Dyer关于该主题的文章。 本文是monads最终为我“点击”时向我解释的内容。
    • Monads的奇迹
  2. 一个很好的说明,为什么您可能想要一个monad (在示例中使用Haskell) 。
    • 您可能已经发明了Monad! (也许您已经拥有了。)丹·皮波尼(Dan Piponi)
  3. 上一篇文章到JavaScript的“翻译”。
    • James Coglan 所读过的有关Monad 最佳入门的精选部分的从Haskell到JavaScript的翻译

#9楼

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

  • return x

    创建一个封装

    x

    的monad类型的值
  • m >>= f

    (将其读取为“绑定运算符”)将函数

    f

    应用于单子

    m

    的值

那就是一个单子。 还有更多技术 ,但是基本上这两个操作定义了monad。 真正的问题是,“一个monad 做什么?”,这取决于monad-列表是monad,Maybes是monad,IO操作是monad。 当我们说这些东西是monad时,这意味着它们具有

return

>>=

的monad接口。

#10楼

如果您曾经使用过Powershell,那么Eric描述的模式应该听起来很熟悉。 Powershell cmdlet是monad。 功能组成用流水线表示。

Jeffrey Snover对Erik Meijer的采访更加详细。

#11楼

为什么我们需要单子?

  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 | Nothing

    代替

    g : Real,Real -> Real

    (从两个实数转换为

    g : Real,Real -> Real

    函数)

    g : Real,Real -> Real | Nothing

    g : Real,Real -> Real | Nothing

    (从两个实数转换为(实数或无数)的功能)。
  4. 但是函数(为了简化)应该只返回一件事 。

    解决方案:让我们创建一种新的要返回的数据类型,即“ 装箱类型 ”,其中可能包含实数或仅是空值。 因此,我们可以有

    g : Real,Real -> Maybe Real

    。 好的但是 ...
  5. 现在

    f(g(x,y))

    什么?

    f

    尚未准备好消耗

    Maybe Real

    。 而且,我们不想更改可以与

    g

    连接的每个函数来消耗

    Maybe Real

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

    在我们的例子中:

    g >>= f

    (将

    g

    连接/组成

    f

    )。 我们想要

    >>=

    得到

    g

    的输出,检查它,如果它为

    Nothing

    就不要调用

    f

    并返回

    Nothing

    。 或相反,提取装箱的

    Real

    并用它输入

    f

    。 (此算法只是

    Maybe

    类型的

    >>=

    的实现)。
  6. 使用此相同的模式可以解决许多其他问题:1.使用“框”来整理/存储不同的含义/值,并具有诸如

    g

    函数来返回这些“框值”。 2.使用作曲家/链接器

    g >>= f

    来帮助将

    g

    的输出连接到

    f

    的输入,因此我们根本不必更改

    f

  7. 使用此技术可以解决的显着问题是:
    • 具有全局状态,函数序列中的每个函数(“程序”)可以共享:解决方案

      StateMonad

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

      IO

      monad。

完全幸福!

#12楼

我会说与单子最接近的OO类比是“ 命令模式 ”。

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

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

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

像这样的更加花哨的执行语义,例如事务,非确定性执行或连续性,都可以用本机不支持的语言来实现。 如果您考虑一下,这是一个非常强大的模式。

现在,实际上,命令模式并未像这样被用作通用语言功能。 将每个语句转换为单独的类的开销将导致大量的样板代码。 但是从原理上讲,它可以用来解决与单子在fp中解决相同的问题。

#13楼

请参阅我对“什么是单子”的回答 。

它从一个具有启发性的示例开始,通过示例进行工作,派生出monad的示例,并正式定义“ monad”。

它假定不具备函数编程知识,并且使用具有

function(argument) := expression

语法和尽可能简单的表达式的伪代码。

此C ++程序是伪代码monad的实现。 (供参考:

M

是类型构造函数,

feed

是“ bind”操作,而

wrap

是“ return”操作。)

#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;
}
           

#14楼

用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?

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

就OO编程而言,monad是一个接口(或更可能是一个mixin),由类型参数化,具有两种方法,

return

bind

描述:

  • 如何注入一个值以获得该注入值类型的单子值;
  • 如何使用将非单值变成单值的函数。

它解决的问题是您从任何接口期望得到的相同类型的问题,即:“我有很多不同的类,它们执行不同的操作,但是似乎以具有根本相似性的方式来执行这些不同的操作。我可以描述它们之间的相似性,即使类本身不是'Object'类本身的真正子类型呢?”

更具体地说,

Monad

“接口”类似于

IEnumerator

IIterator

,因为它采用的类型本身就是一种类型。 尽管

Monad

的主要“要点”是能够连接基于内部类型的操作,甚至可以连接到具有新“内部类型”的要点,同时保留(甚至增强)主要类的信息结构。

#15楼

您最近有克里斯托弗·联盟 ( Christopher League )的演讲“ Monadologie-缓解类型焦虑的专业帮助 ”(2010年7月12日),该主题对延续和monad主题非常有趣。

与此演示文稿一起播放的视频实际上可在 vimeo上获得 。

在这段1小时的视频中,Monad部分的开始时间约为37分钟,从58张幻灯片的幻灯片42开始。

它被表示为“函数式编程的领先设计模式”,但是示例中使用的语言是Scala,它既是OOP又是函数式的。

您可以从Debasish Ghosh (2008年3月27日)的博客文章“ Monads- Scala中抽象计算的另一种方法 ”中阅读有关Scala中Monad的更多信息。

如果类型构造函数 M支持以下操作,则它是monad:
# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def 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

    是单子
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 => None
       case 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 => Nil
        case x::xs => f(x) ::: flatMap(xs)(f)
      }      

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

for

Scala的理解 :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 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

调用都返回相同的数据结构类型(但值不同),用作链中下一个命令的输入。

在以上代码段中,flatMap将闭包

(SomeType) => List[AnotherType]

作为输入,并返回

List[AnotherType]

。 需要注意的重要一点是,所有flatMap都采用与输入相同的闭包类型,并作为输出返回相同的类型。

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

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

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

但是如果不利用Monad,您将得到复杂的OOP代码,例如:

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

而Monad则可以像所有操作一样使用实际类型(

Venue

User

),并使Option验证内容保持隐藏,这都是因为for语法的平面图:

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

仅当所有三个函数都具有

Some[X]

,才会执行yield部分。 任何

None

都将直接返回以

confirm

所以:

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

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

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

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

范畴理论提出了许多计算模型。 其中
  • 计算的箭头模型
  • Monad计算模型
  • 计算的应用模型

#16楼

为了尊重快速的读者,我首先从精确的定义开始,继续快速地进行更多的“普通英语”解释,然后再转向示例。

这是一个简洁而精确的定义,略有修改:

monad (在计算机科学中)在形式上是以下地图:
  • 将某种给定编程语言的每个类型

    X

    发送到新的类型

    T(X)

    (称为“在

    X

    具有值的

    T

    计算类型”);
  • 配备了将

    f:X->T(Y)

    g:Y->T(Z)

    形式的两个函数组成函数

    g∘f:X->T(Z)

  • 相对于给定的称为

    pure_X:X->T(X)

    单位函数,在明显的意义上是

    pure_X:X->T(X)

    ,并且被视为是将值带到单纯返回该值的纯计算中。

简单来说, monad是从任何类型

X

传递到另一个类型

T(X)

的规则,也是从两个函数

f:X->T(Y)

g:Y->T(Z)

(您想组成但不能创建)到新函数

h:X->T(Z)

。 但是, 这并不是严格意义上的数学构成。 我们基本上是在“弯曲”功能的组成或重新定义功能的组成方式。

另外,我们要求构成monad的规则可以满足“显而易见的”数学公理:

  • 关联性 :将

    f

    g

    ,然后与

    h

    (从外部)组成,应与

    g

    h

    组成,然后与

    f

    (从内部)组成。
  • 单位性质 :将

    f

    与任一侧的恒等函数组成应产生

    f

再次,用简单的话来说,我们不能只是疯狂地重新定义我们喜欢的函数组成:

  • 我们首先需要关联性,以便能够连续地组合几个函数,例如

    f(g(h(k(x)))

    ,而不用担心指定组成函数对的顺序,因为monad规则仅规定了如何组合一对函数 ,没有那个公理,我们需要知道哪个对首先组成,依此类推(注意,与由

    g

    组成的

    f

    与由

    f

    组成的

    g

    相同的可交换性不同。 )。
  • 第二,我们需要单位属性,这就是说身份按照我们期望的方式琐碎地组成。 因此,只要可以提取这些身份,我们就可以安全地重构函数。

再次简单地说:monad是类型扩展和组成满足两个公理(关联性和单位属性)的函数的规则。

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

简而言之,就是如此。

作为专业的数学家,我宁愿避免将

h

称为

f

g

的“组成”。 因为从数学上来说不是。 称其为“组成”是错误地假设

h

是真正的数学组成,而事实并非如此。 它甚至不是由

f

g

唯一确定的。 相反,这是我们monad新的“组成规则”功能的结果。 即使实际的数学构成存在,这也可能与实际的数学构成完全不同!

Monad 不是函子 ! 函子

F

是从类型

X

到类型

F(X)

以及在类型

X

Y

之间的函数(态射)到函数

F(X)

F(Y)

之间的函数(将对象发送到对象并将它们的态射向态射影)的规则。类别理论)。 相反,monad将一对函数

f

g

发送给新的

h

为了减少干燥,让我尝试通过示例进行说明,以小节进行注释,因此您可以直接跳过这一点。

抛出异常作为Monad示例

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

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

但是未定义

f(0)

,因此引发了异常

e

。 然后,如何定义构图值

g(f(0))

? 当然,再次抛出异常! 也许是相同的

e

。 也许是新的更新异常

e1

这到底发生了什么? 首先,我们需要新的异常值(不同或相同)。 您可以将它们命名为

nothing

null

或其他任何东西,但本质保持不变,它们应为新值,例如,在我们的示例中,它不应为

number

。 我不希望将它们称为

null

以避免与如何在任何特定语言中实现

null

混淆。 同样,我更喜欢避免

nothing

因为它通常与

null

相关联,从原则

null

,这是

null

应该做的事情,但是,无论出于何种实际原因,该原则通常都会被屈服。

到底什么是例外?

对于任何经验丰富的程序员来说,这都是一件微不足道的事情,但是我想说几句话只是为了消除任何混乱的蠕虫:

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

范围包括丢弃任何详细信息并返回单个全局值(如

NaN

null

)或生成长日志列表或确切发生的情况,将其发送到数据库并在整个分布式数据存储层进行复制;)

这两个极端的例外示例之间的重要区别在于,在第一种情况下, 没有副作用 。 在第二个中。 这使我们想到了(千美元)问题:

纯函数中是否允许例外?

简短的回答 :是的,但前提是它们不会导致副作用。

更长的答案。 纯粹来说,函数的输出必须由其输入唯一地确定。 因此,我们通过向新的抽象值

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

不是一种在函数组成方面表现出我们想要的方式的类型,而诸如

null

NaN

类的实际类型却并非如此,而是经过了一些人为的和并非总是直观的类型转换。

类型扩展

当我们想改变异常内部的消息时,我们实际上是为整个异常对象声明一个新的类型

E

,然后,

maybe number

做了什么,除了其令人困惑的名称之外,它要么是

number

类型,要么是新的异常类型

E

,所以它实际上是联合

number | E

number | E

number

E

。 特别是,这取决于我们要如何构造

E

,这既没有建议也没有反映在名称

maybe number

什么是功能成分?

这是一种数学运算,它使用函数

f: X -> Y

g: Y -> Z

并将其组成构造为函数

h: X -> Z

满足

h(x) = g(f(x))

。 当不允许将结果

f(x)

用作

g

参数时,将发生此定义的问题。

在数学中,如果没有额外的工作,这些功能就无法组成。 对于上述

f

g

示例,严格的数学解决方案是从

f

的定义集中删除

。 有了新的定义集(

x

新限制类型),

f

就可以与

g

但是,在程序设计中像这样限制

f

的定义集不是很实际。 而是可以使用异常。

或者作为另一种方法,将创建人工值,例如

NaN

undefined

null

Infinity

等。因此,您将

1/0

评估为

Infinity

,将

1/-0

评估为

-Infinity

。 然后将新值强制返回到表达式中,而不是引发异常。 导致结果的您可能会或可能无法预测:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1
           

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

JavaScript使我们能够不惜一切代价继续执行数值表达式,而不会像上面的示例中那样引发错误。 这意味着,它还可以组成功能。 这正是monad的含义-构成满足此答案开头定义的公理的函数是规则。

但是,构成函数的规则是否源于JavaScript用于处理数字错误的实现,而又是monad?

要回答这个问题,您所需要的只是检查公理(此处不作为练习的一部分保留为练习;)。

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

实际上,一个更有用的monad会是一条规则,该规则规定,如果

f

对某些

x

抛出异常,则其与任何

g

组合也是如此。 另外,使异常

E

全局唯一,永远只有一个可能的值(类别理论中的终端对象 )。 现在,这两个公理立即可检查,我们得到了一个非常有用的单子。 结果就是众所周知的monad 。

#17楼

用面向对象的术语来说,monad是一个流畅的容器。

最低要求是

class <A> Something

的定义,

class <A> Something

支持构造函数

Something(A a)

和至少一个方法

Something<B> flatMap(Function<A, Something<B>>)

可以说,它还可以计算monad类是否具有签名为

Something<B> work()

方法,这些方法可以保留类的规则-编译器在编译时会在flatMap中进行烘焙。

为什么monad有用? 因为它是一个容器,允许保留语义的可链接操作。 例如,

Optional<?>

保留isPresent的语义,以用于

Optional<String>

Optional<Integer>

Optional<MyClass>

等。

作为一个粗略的例子

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

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

在OO中,可能需要花费一些时间,但是Something上任何返回Something的另一个子类的方法都符合返回原始类型的容器的容器函数的标准。

这就是您保留语义的方式-即容器的含义和操作没有改变,它们只是包装和增强了容器内的对象。

#18楼

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

我所知道的并没有这样的解释,并且存在一个存在的建议掩盖了OOP程序员的傲慢。

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

Monads使用统一的界面解决了许多不同的问题,这就是它们有用的原因。 他们使用的主要是懒惰 - 它们允许具有副作用的懒惰功能,这对FFI等很重要。 它们还常用于错误处理和解析。

为了澄清我一直在寻找的理解,让我们假设您正在将具有monad的FP应用程序转换为OOP应用程序。 你会怎么做把monad的职责移植到OOP应用程序?

由于懒惰在OOP中并不常见,因此在大多数情况下它们都是无关紧要的。 但是,您可以移植monadic错误处理。

#19楼

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

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

  1. 下面的功能组合是可传递的。 因此,从A到CI可以组成A => B和B =>C。
A => C   =   A => B  andThen  B => C
           
Monad用简单的英语? (对于没有FP背景的OOP程序员)
  1. 但是,如果函数返回的效果类型为Option [A],即A => F [B],则该组合对于转到B无效,我们需要A => B,但我们需要A => F [B]。
    Monad用简单的英语? (对于没有FP背景的OOP程序员)
    我们需要一个特殊的运算符“ bind”,该运算符知道如何融合这些返回F [A]的函数。
A => F[C]   =   A => F[B]  bind  B => F[C]
           

为特定的F定义了“绑定”功能。

还为任何A定义了A => F [A]类型的“返回” ,也为该特定F定义了“返回” 。 要成为Monad, F必须为其定义这两个功能。

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

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

但是给定的F也可以定义自己的不透明的“内置”特殊功能,此类功能使用户无法自行定义(使用纯语言),例如

  • “ random”( 范围=> Random [Int] )
  • “ print”( 字符串=> IO [()] )
  • “尝试...抓住”等

#20楼

我正在分享我对Monad的理解,这在理论上可能并不完美。 Monad是关于上下文传播的 。 Monad是,您为某些数据定义一些上下文,然后定义如何在整个处理管道中将上下文随数据一起携带。 定义上下文传播主要是关于定义如何合并多个上下文。 它还可以确保不会从数据中意外剥离上下文。 这个简单的概念可以用来确保程序的编译时间正确性。

#21楼

我能想到的最简单的解释是,单子是将函数与具有良好结果的函数(也称为Kleisli合成)进行组合的一种方式。 “嵌入”功能的签名为

a -> (b, smth)

,其中

a

b

是可能彼此不同的类型(如

Int

Bool

),但不一定-且

smth

是“上下文”或“点缀”。

这种类型的函数也可以写成a-

a -> mb

,其中

m

等于“ embelishment”

smth

。 因此,这些函数是在上下文中返回值的函数(想想记录其动作(其中

smth

是记录消息)的函数;执行输入\\输出且其结果取决于IO动作的结果的函数)。

monad是一个接口(“ typeclass”),它使实现者告诉其如何组成此类功能。 实现者需要为想要实现接口的任何类型

m

定义一个合成函数

(a -> mb) -> (b -> mc) -> (a -> mc)

a-

(a -> mb) -> (b -> mc) -> (a -> mc)

(这是Kleisli合成)。

因此,如果我们说有一个元组类型

(Int, String)

表示对

Int

的计算结果,它们也记录了它们的动作,其中

(_, String)

是“实现”-动作的日志-和两个函数我们想获得一个函数

increment :: Int -> (Int, String)

twoTimes :: Int -> (Int, String)

incrementThenDouble :: Int -> (Int, String)

twoTimes :: Int -> (Int, String)

incrementThenDouble :: Int -> (Int, String)

这是两个函数考虑到日志。

在给定的示例中,两个函数的monad实现均适用于整数2的

twoTimes (increment 2)

incrementThenDouble 2

(等于

twoTimes (increment 2)

)将返回

(6, " Adding 1. Doubling 3.")

increment 2

(6, " Adding 1. Doubling 3.")

中间结果

increment 2

等于到

(3, " Adding 1.")

twoTimes 3

等于

(6, " Doubling 3.")

从这一克莱斯里合成函数可以推导出通常的一元函数。

继续阅读