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

其他回答

可选/可能是最基本的一元类型

单子是关于功能组成的。如果函数f:可选<A>->可选<B>,g:可选<B>->可选<C>,h:可选<C>->可选<D>。然后你可以创作它们

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

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

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

并且可以写作文

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

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

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

范围的一元特征是我们可以变换然后变平,即从一个整数范围内编码的表示开始[36,98]

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

然后压平[am','a','c','h','i','n','e','l',''e'

而不是编写此代码

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循环(无符号键:rng)向上推到绑定函数中,从而允许理论上更容易推理的代码。毕达哥拉斯三元组可以在范围-v3中使用嵌套绑定生成(而不是我们看到的可选的链式绑定)

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

为什么我们需要单子?

我们只想使用函数编程。(毕竟是“功能编程”-FP)。然后,我们遇到了第一个大问题。这是一个程序:f(x)=2*xg(x,y)=x/y我们怎么能说首先要执行什么?我们如何使用不超过个函数来形成一个有序的函数序列(即程序)?解决方案:组合函数。如果你先要g,然后要f,只需写f(g(x,y))。好的,但是。。。更多问题:某些函数可能会失败(即g(2,0),除以0)。我们在FP中没有“例外”。我们如何解决它?解决方案:让我们允许函数返回两种东西:而不是g:Real,Real->Real(函数从两个实数转换为实数),让我们允许g:Real、Real->Real|Nothing(函数从一个实数转换成(实数或零))。但函数应该(更简单地)只返回一件事。解决方案:让我们创建一种要返回的新类型的数据,一种“装箱类型”,它可能包含一个真实的数据,也可能只是一个空数据。因此,我们可以有g:真实,真实->可能真实。好的,但是。。。f(g(x,y))现在发生了什么?f还没有准备好使用“也许真的”。而且,我们不想改变我们可以与g连接的每一个函数,以使用Maybe Real。解决方案:让我们有一个特殊的函数来“连接”/“组合”/“链接”函数。这样,我们就可以在幕后调整一个函数的输出,以支持下一个函数。在我们的例子中:g>>=f(连接/合成g到f)。我们希望>>=获取g的输出,检查它,如果它是Nothing,则不要调用f并返回Nothing;或者相反,提取装箱的实数并用它来馈送f。(此算法只是Maye类型的>>=的实现)。出现了许多其他问题,可以使用相同的模式来解决:1。使用“框”来编码/存储不同的含义/值,并具有像g这样的函数来返回这些“框值”。2.让作曲家/链接器g>>=f帮助将g的输出连接到f的输入,这样我们就不必改变f。使用该技术可以解决的显著问题有:具有函数序列中的每个函数(“程序”)可以共享的全局状态:解StateMonad。我们不喜欢“不纯函数”:对相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回一个标记/装箱的值:IOmonad。

完全幸福!!!!

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

按照OOP程序员将理解(没有任何功能编程背景),什么是莫纳德?它解决了什么问题是最常用的地方吗?是最常用的地方吗?

就OO编程而言,monad是一个接口(或者更可能是一个mixin),由一个类型参数化,具有两个方法,return和bind,它们描述:

如何注入值以获得注入值的一元值类型如何使用从非一元值。

它解决的问题与您期望的任何接口的问题类型相同,“我有很多不同的类,它们做不同的事情,但似乎以一种具有潜在相似性的方式来做这些不同的事情。即使这些类本身不是比‘Object’类本身更接近的子类,我如何描述它们之间的相似性?”

更具体地说,Monad“接口”与IEnumerator或IIterator相似,因为它采用的类型本身也采用的类型。然而,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)