就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
为什么我们需要单子?
我们只想使用函数编程。(毕竟是“功能编程”-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。
完全幸福!!!!
其他回答
在OO术语中,monad是一个流畅的容器。
最低要求是类<a>的定义,它支持构造函数Something(a a)和至少一个方法Something<B>flatMap(函数<a,Something<B>>)
可以说,monad类是否有签名Something<B>work()的方法来保存类的规则——编译器在编译时在flatMap中烘焙。
为什么单子有用?因为它是一个允许保留语义的可链式操作的容器。例如,可选<?>为Optional<String>、Optional<Integer>、Optional<MyClass>等保留isPresent的语义。
作为一个粗略的例子,
Something<Integer> i = new Something("a")
.flatMap(doOneThing)
.flatMap(doAnother)
.flatMap(toInt)
注意,我们以字符串开头,以整数结尾。很酷。
在OO中,这可能需要一点努力,但Something上的任何方法如果返回Something的另一个子类,都符合返回原始类型容器的容器函数的标准。
这就是保持语义的方式——即容器的含义和操作不会改变,它们只是包装和增强容器内的对象。
monad在OO中是否具有“自然”解释取决于monad。在像Java这样的语言中,您可以将may monad转换为检查空指针的语言,这样失败的计算(即,在Haskell中生成Nothing)会将空指针作为结果发出。您可以将状态monad转换为通过创建可变变量和更改其状态的方法生成的语言。
monad是内函子范畴中的幺半群。
这句话所表达的信息非常深刻。你在一个monad中使用任何命令式语言。monad是一种“有序”的领域特定语言。它满足某些有趣的财产,这些属性使单子函数成为“命令式编程”的数学模型。Haskell使定义小型(或大型)命令式语言变得容易,这些语言可以以多种方式组合。
作为一名OO程序员,您使用语言的类层次结构来组织可以在上下文中调用的函数或过程的类型,即您所称的对象。monad也是对这个概念的抽象,因为不同的monad可以以任意方式组合,有效地将所有子monad的方法“导入”到范围中。
从体系结构上讲,然后使用类型签名来明确表示可以使用哪些上下文来计算值。
可以使用monad转换器来实现这一目的,并且有一个高质量的所有“标准”monad集合:
列表(通过将列表视为域进行非确定性计算)可能(计算可能失败,但报告不重要)错误(可能失败并需要异常处理的计算Reader(可以由普通Haskell函数组合表示的计算)编写器(使用顺序“渲染”/“记录”(到字符串、html等)进行计算)续(续)IO(取决于底层计算机系统的计算)状态(上下文包含可修改值的计算)
具有相应的monad变压器和类型类别。类型类允许通过统一monad的接口来组合monad,从而使具体monad可以实现monad“类”的标准接口。例如,模块Control.Monad.State包含一个类MonadState s m,(State s)是表单的一个实例
instance MonadState s (State s) where
put = ...
get = ...
长话短说,monad是一个函子,它将“上下文”附加到一个值上,它可以向monad中注入一个值,并且可以根据附加到它上的上下文来评估值,至少是以受限的方式。
So:
return :: a -> m a
是一个函数,它将a类型的值注入到m类型的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 <- m
y <- f x
g y
(>>)的类型可能是发光的。它是
(>>) :: m a -> m b -> m b
它对应于C等过程语言中的(;)运算符。它允许使用以下表示法:
m = do x <- someQuery
someAction x
theNextAction
andSoOn
在数学和哲学逻辑中,我们有框架和模型,这些框架和模型“自然”地用单子主义建模。解释是一种函数,它查看模型的域,并计算命题(或公式,在推广下)的真值(或推广)。在必要性的模态逻辑中,我们可能会说,如果命题在“每个可能的世界”中都是真的,那么它是必要的——如果它在每个可容许的域中都是真实的。这意味着命题语言中的模型可以具体化为一个模型,其域由不同模型的集合组成(一个对应于每个可能的世界)。每个monad都有一个名为“join”的方法,该方法将分层,这意味着结果为monad动作的每个monad动作都可以嵌入到monad中。
join :: m (m a) -> m a
更重要的是,这意味着monad在“层堆叠”操作下关闭。这就是monad转换器的工作原理:它们通过为以下类型提供“类联接”方法来组合monad:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
这样我们就可以将(MaybeT m)中的作用转换为m中的作用,有效地折叠层。在本例中,runMaybeT::MaybeT m a->m(Maybe a)是我们的类联接方法。(MaybeT m)是一个monad,MaybeT::m(Maybe a)->MaybeT ma实际上是m中一个新类型monad动作的构造函数。
函子的自由单元是通过堆叠f生成的单元,这意味着f的每个构造函数序列都是自由单元的元素(或者更确切地说,是与f的构造函数序列树形状相同的元素)。自由单体是一种用最少的锅炉板构建柔性单体的有用技术。在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))
Monastem是您可能称之为“解释器”或“命令”模式的基础架构,抽象为最清晰的形式,因为每个单元计算都必须“运行”,至少是微不足道的。(运行时系统为我们运行IO monad,是任何Haskell程序的入口点。IO通过按顺序运行IO操作来“驱动”其余的计算)。
join的类型也是我们得到monad是内函子范畴中的幺半群的陈述的地方。由于其类型,联接对于理论目的来说通常更为重要。但了解类型意味着了解单子。Join和monad变换器的类Join类型在函数组合的意义上是内函子的有效组合。把它放在类似Haskell的伪语言中,
Foo::m(m a)<->(m.m)a
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[()])“尝试…捕捉”等。
更新:这个问题是一个非常长的博客系列的主题,你可以在Monads上阅读-谢谢你提出的问题!
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
monad是一种类型的“放大器”,它遵守某些规则,并提供某些操作。
首先,什么是“类型放大器”?我指的是某种系统,它可以让你选择一种类型,并将其转换为更特殊的类型。例如,在C#中,考虑Nullable<T>。这是一种类型的放大器。它允许您接受一个类型,比如int,并为该类型添加一个新的功能,即现在它可以在以前不能为null时为null。
作为第二个例子,考虑IEnumerable<T>。这是一种类型的放大器。它允许您获取一个类型,例如字符串,并为该类型添加一个新功能,即您现在可以从任意数量的单个字符串中创建一个字符串序列。
什么是“特定规则”?简言之,对于基础类型上的函数来说,有一种合理的方法来处理放大的类型,从而使它们遵循函数组合的正常规则。例如,如果你有一个关于整数的函数,比如
int M(int x) { return x + N(x * 2); }
则Nullable<int>上的相应函数可以使其中的所有运算符和调用“以与之前相同的方式”一起工作。
(这是难以置信的模糊和不精确;你要求的解释没有假设任何关于功能成分的知识。)
什么是“操作”?
有一个“单元”操作(有时被混淆地称为“返回”操作),它从普通类型中获取值并创建等效的一元值。本质上,这提供了一种获取未放大类型值并将其转换为放大类型值的方法。它可以作为OO语言中的构造函数实现。有一个“绑定”操作,它接受一个一元值和一个可以转换该值的函数,并返回一个新的一元值。Bind是定义monad语义的关键操作。它允许我们将未放大类型上的操作转换为放大类型的操作,这符合前面提到的函数组合规则。通常有一种方法可以使未放大的类型从放大的类型中恢复出来。严格来说,这个操作不需要有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);
}
通过Value属性可以将Nullable<int>转换回int。
关键是函数转换。请注意,在转换中如何捕获可为null的操作的实际语义(即对null的操作传播null)。我们可以概括这一点。
假设你有一个从int到int的函数,就像我们最初的M一样。你可以很容易地将它转换成一个接受int并返回Nullable<int>的函数,因为你可以通过可空构造函数来运行结果。现在假设你有一个更高阶的方法:
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,或者接受一个整型并返回一一个Nullable<int>的方法现在都可以应用可空语义。
此外:假设您有两种方法
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); }
monad上的绑定操作使放大类型上的函数组合工作。我在上面写的“规则”是,monad保留了正常函数组合的规则;使用同一函数的合成产生原始函数,合成是关联的,等等。
在C#中,“Bind”被称为“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;
}
可为null的monad规则是“将产生可为null值的两个函数组合在一起,检查内部函数是否产生null值;如果产生null值,则产生null值。如果没有,则调用外部函数产生null值”。这就是nullable的理想语义。
序列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;
}
现在我们可以说“将这一组单独的整数放大成一系列整数。将这一特定的整数转换成一系列字符串,放大成一组字符串。现在将这两个操作放在一起:将这一系列整数放大成所有字符串序列的串联。”单子允许您合成放大。
它解决了什么问题,最常用的地方是什么?
这相当于问“单例模式解决了什么问题?”,但我会尝试一下。
Monad通常用于解决以下问题:
我需要为这种类型创建新的功能,并且仍然在这种类型上组合旧的功能以使用新的功能。我需要在类型上捕获一堆操作,并将这些操作表示为可组合对象,构建越来越大的组合,直到表示出正确的一系列操作,然后我需要开始获得结果我需要用讨厌副作用的语言清晰地表示副作用操作
C#在设计中使用monad。如前所述,可空模式与“可能是monad”非常相似。LINQ完全由monad构建;SelectMany方法是操作组合的语义工作。(Erik Meijer喜欢指出,每个LINQ函数实际上都可以由SelectMany实现;其他一切都只是为了方便。)
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序中?
大多数OOP语言没有足够丰富的类型系统来直接表示monad模式本身;您需要一个支持高于泛型类型的类型的类型系统。所以我不会这么做。相反,我将实现表示每个monad的泛型类型,并实现表示您需要的三个操作的方法:将值转换为放大值,(可能)将放大值转换为值,以及将未放大值上的函数转换为放大的值上的函数。
一个好的开始是我们如何在C#中实现LINQ。研究SelectMany方法;这是理解序列monad如何在C#中工作的关键。这是一个非常简单的方法,但非常强大!
建议进一步阅读:
为了对C#中的单子进行更深入、理论上更合理的解释,我强烈推荐我(埃里克·里佩尔)的同事韦斯·戴尔(Wes Dyer)关于这个主题的文章。这篇文章是当他们最终为我“点击”时向我解释单子的原因。蒙得斯的奇迹这是一个很好的例子,说明了为什么您可能需要一个monad(在示例中使用Haskell)。你本可以发明修道院的!(也许你已经有了。)丹·皮波尼有点像,将上一篇文章“翻译”为JavaScript。詹姆斯·科格兰(James Coglan)所读过的monads最佳简介精选部分从Haskell翻译成JavaScript