为什么我们需要monads?
在我的愚见中,对于着名问题“什么是单子?”的回答,尤其是最被投票的问题,试图解释什么是单子,而没有明确解释为什么单子是真正必要的。 他们能解释为解决问题的方法吗?
为什么我们需要monads?
那么,我们有第一个大问题。 这是一个程序:
f(x) = 2 * x
g(x,y) = x / y
我们怎么说先执行什么? 我们如何使用不超过函数形成一个有序的函数序列(即程序 )?
解决方案: 编写功能 。 如果你想先g
然后f
,只需写f(g(x,y))
。 这样,“程序”也是一个函数: main = f(g(x,y))
。 好的但是 ...
更多的问题:一些函数可能会失败 (即g(2,0)
,除以0)。 我们在FP中没有“例外” (例外不是函数)。 我们如何解决它?
解决方案:我们让函数返回两种东西 :代替g : Real,Real -> Real
(从两个实数到实数的函数),让我们让g : Real,Real -> Real | Nothing
g : Real,Real -> Real | Nothing
(从两个实际功能(实际或没有))。
但函数应该(更简单)只返回一件事 。
解决方案:让我们创建一个新类型的数据来返回,一个“ 拳击类型 ”,可能包含一个真实或简直没有。 因此,我们可以有g : Real,Real -> Maybe Real
。 好的但是 ...
现在发生什么f(g(x,y))
? f
并没有准备好消耗Maybe Real
。 而且,我们不想改变我们可以用g
连接的每一个函数来使用Maybe Real
。
解决方案:让我们有一个特殊的功能来“连接”/“撰写”/“链接”功能 。 这样,我们可以在幕后调整一个函数的输出来输入下面的函数。
在我们的例子中: g >>= f
(连接/合成g
到f
)。 我们希望>>=
获得g
的输出,检查它,并在情况下,它是Nothing
只是不叫f
并返回Nothing
; 或者相反,提取盒装的Real
和饲料f
。 (这个算法仅仅是>>=
对于Maybe
类型的实现)。 另请注意, >>=
必须每个“装箱类型”(不同的箱子,不同的适配算法)写入一次 。
许多其他问题都可以通过使用相同的模式来解决:1.使用“盒子”来编码/存储不同的含义/值,并且具有像g
这样的函数来返回这些“盒装值”。 2.有一个作曲家/连接器g >>= f
帮助将g
的输出连接到f
的输入,所以我们根本不需要改变任何f
。
使用这种技术可以解决的显着问题是:
具有全局状态,即函数序列中的每个函数(“程序”)都可以共享:解决方案StateMonad
。
我们不喜欢“不纯的功能”:对同一输入产生不同输出的功能。 因此,让我们标记这些函数,使它们返回一个标记/盒装值: IO
monad。
总幸福!
答案当然是, “我们不” 。 与所有抽象一样,这不是必需的。
Haskell不需要monad抽象。 以纯语言执行IO不是必需的。 IO
类型自己处理就好了。 现有的单块解除do
块可以用GHC.Base
模块中定义的bindIO
, returnIO
和failIO
来GHC.Base
。 (这不是一个记录在hackage上的模块,所以我不得不指出它的源文件。)所以不,不需要monad抽象。
所以如果不需要,它为什么存在? 因为发现许多计算模式形成一元结构。 抽象结构允许编写可以在该结构的所有实例中工作的代码。 简单地说 - 代码重用。
在函数式语言中,为代码重用找到的最强大的工具就是函数的组合。 好的(.) :: (b -> c) -> (a -> b) -> (a -> c)
运算符是非常强大的。 它可以轻松编写小函数,并以最少的语法或语义开销将它们粘合在一起。
但有些情况下这些类型工作不正确。 当你有foo :: (b -> Maybe c)
和bar :: (a -> Maybe b)
? foo . bar
foo . bar
不typecheck,因为b
和Maybe b
不是同一类型。
但是......几乎是正确的。 你只需要一点余地。 你希望能够将Maybe b
当作基本b
来对待。 不过,把它们作为同类型对待它是一个糟糕的主意。 这与托尼霍尔着名称为十亿美元的错误的空指针差不多。 所以如果你不能把它们当作同一类型,也许你可以找到一种扩展组合机制(.)
规定的方法。
在这种情况下,真正研究(.)
基础是非常重要的。 幸运的是,有人已经为我们做了这个。 事实证明, (.)
和id
的组合形成了一个称为类别的数学结构。 但是还有其他方式可以形成类别。 例如,Kleisli类别允许组成的对象稍微扩大一点。 一个Kleisli类的Maybe
可能包含(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
和id :: a -> Maybe a
。 也就是说,类别中的对象用一个Maybe
增加(->)
,所以(a -> b)
变成(a -> Maybe b)
。
突然之间,我们已经将构图的力量扩展到传统(.)
操作无法解决的问题。 这是新抽象力量的来源。 Kleisli类别的工作类型不仅仅是Maybe
。 他们与每一种可以组合适当类别的类型合作,遵守类别法律。
id . f
id . f
= f
f . id
f . id
= f
f . (g . h)
f . (g . h)
= (f . g) . h
(f . g) . h
只要你能证明你的类型服从这三个法则,你就可以把它变成一个Kleisli类别。 那有什么大不了的? 那么,事实证明monads和Kleisli类别完全一样。 Monad
的return
是一样的Kleisli id
。 Monad
的(>>=)
与Kleisli (.)
不完全相同,但是从另一个角度来看,它们很容易编写。 当你将它们翻译为(>>=)
和(.)
之间的差异时,类别法则与monad法则相同。
那么为什么要经历这一切呢? 为什么要在语言中使用Monad
抽象? 正如我上面提到的那样,它支持代码重用。 它甚至可以在两个不同的维度上重用代码。
代码重用的第一个维度直接来自抽象的存在。 您可以编写适用于所有抽象实例的代码。 整个monad-loop软件包由可与任何Monad
实例一起工作的循环组成。
第二个维度是间接的,但是从构成的存在来看。 当组成很容易时,用小的可重用块编写代码是很自然的。 这与用函数的(.)
运算符鼓励编写小的可重用函数是一样的。
那么为什么抽象存在? 因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码并鼓励创建更多可重用的代码。 代码重用是编程的圣杯之一。 单子抽象的存在是因为它让我们有点朝着这个圣杯前进。
本杰明皮尔斯在TAPL中表示
类型系统可以被认为是计算一种程序中术语运行时行为的静态近似。
这就是为什么配备了强大类型系统的语言严格地比表达性差,而不是类型差的语言。 你可以用同样的方式思考单子。
作为@Carl和sigfpe点,你可以装备一个数据类型的所有你想要的操作,而不诉诸monad,typeclasses或任何其他抽象的东西。 然而monads不仅可以编写可重用的代码,还可以抽取所有冗余的详细信息。
作为一个例子,假设我们要过滤一个列表。 最简单的方法是使用filter
函数: filter (> 3) [1..10]
,等于[4,5,6,7,8,9,10]
。
是一个稍微复杂一点的filter
,也是从左到右传递一个累加器
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
为了得到所有的i
,使得i <= 10, sum [1..i] > 4, sum [1..i] < 25
,我们可以写出
filterAccum (a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
相当于[3,4,5,6]
。
或者我们可以重新定义根据filterAccum
从列表中删除重复元素的nub
函数:
nub' = filterAccum (a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
等于[1,2,4,5,3,8,9]
。 列表在这里作为累加器传递。 代码的工作原理是,因为有可能离开列表monad,所以整个计算保持纯粹( notElem
实际上不使用>>=
但它可以)。 然而,不可能安全地离开IO monad(即,您不能执行IO操作并返回一个纯粹的值 - 该值始终会包装在IO monad中)。 另一个例子是可变数组:在你离开ST monad后,一个可变数组存在,你不能再以恒定的时间更新数组。 所以我们需要一个来自Control.Monad
模块的monadic过滤器:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM
为列表中的所有元素执行filterM
操作,生成单元操作返回True
元素。
数组的过滤示例:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
按预期打印[1,2,4,5,3,8,9]
。
还有一个IO monad版本,它询问要返回哪些元素:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
例如
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
并作为最终的插图, filterAccum
可以在以下方面定义filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
与StateT
monad一样,在引擎盖下使用,仅仅是一个普通的数据类型。
这个例子说明,monad不仅可以抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,正如@Carl所解释的),而且还可以统一处理用户定义的数据类型和内置基元。
链接地址: http://www.djcxy.com/p/42915.html