为什么我们需要monads?

在我的愚见中,对于着名问题“什么是单子?”的回答,尤其是最被投票的问题,试图解释什么是单子,而没有明确解释为什么单子是真正必要的。 他们能解释为解决问题的方法吗?


为什么我们需要monads?

  • 我们只想使用函数进行编程。 (毕竟是“函数式编程(FP)”)。
  • 那么,我们有第一个大问题。 这是一个程序:

    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 (连接/合成gf )。 我们希望>>=获得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模块中定义的bindIOreturnIOfailIOGHC.Base 。 (这不是一个记录在hackage上的模块,所以我不得不指出它的源文件。)所以不,不需要monad抽象。

    所以如果不需要,它为什么存在? 因为发现许多计算模式形成一元结构。 抽象结构允许编写可以在该结构的所有实例中工作的代码。 简单地说 - 代码重用。

    在函数式语言中,为代码重用找到的最强大的工具就是函数的组合。 好的(.) :: (b -> c) -> (a -> b) -> (a -> c)运算符是非常强大的。 它可以轻松编写小函数,并以最少的语法或语义开销将它们粘合在一起。

    但有些情况下这些类型工作不正确。 当你有foo :: (b -> Maybe c)bar :: (a -> Maybe b)foo . bar foo . bar不typecheck,因为bMaybe 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类别完全一样。 Monadreturn是一样的Kleisli idMonad(>>=)与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

    上一篇: Why do we need monads?

    下一篇: IDs from State Monad in Haskell