Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern

   Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming。这其中透露的Monad重要性则不言而喻。Scalaz是通过Monad typeclass为数据运算的程序提供了一套规范的编程方式,如常见的for-comprehension。而不同类型的Monad实例则会支持不同的程序运算行为,如:Option Monad在运算中如果遇到None值则会中途退出;State Monad会确保状态值会伴随着程序运行流程直到终结;List Monad运算可能会产生多个结果等等。Scalaz提供了很多不同种类的Monad如:StateMonad, IOMonad, ReaderMonad, WriterMonad,MonadTransformer等等,这从另一个角度也重申了Monad概念在泛函编程里的重要性。听起来以上这些描述好像有点摸不着头脑,可能应该把它们放在本篇最终总结,不过我还是想让大家有个大的概念。对下面的讨论细节的理解能有所帮助。我们还是从Monad trait开始介绍吧:

trait Monad[F[_]] extends Applicative[F] with Bind[F] { self =>
  //// scalaz/Monad.scala

  override def map[A,B](fa: F[A])(f: A => B) = bind(fa)(a => point(f(a)))
...
trait Applicative[F[_]] extends Apply[F] { self =>
  //// scalaz/Applicative.scala
  def point[A](a: => A): F[A]
...
trait Apply[F[_]] extends Functor[F] { self =>
  //// scalaz/Apply.scala
  def ap[A,B](fa: => F[A])(f: => F[A => B]): F[B]
...
trait Bind[F[_]] extends Apply[F] { self =>
  //// scalaz/Bind.scala

  /** Equivalent to `join(map(fa)(f))`. */
  def bind[A, B](fa: F[A])(f: A => F[B]): F[B]

  override def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B] = {
    lazy val fa0 = fa
    bind(f)(map(fa0))
  }
...

上面这些类型trait的继承关系是这样的:Monad继承了Applicative和Bind,Applicative继承了Apply, Apply继承了Functor, Bind也继承了Apply。所以Monad同时又是Applicative和Functor,因为Monad实现了map和ap函数。一个Monad实例可以调用所有Applicative和Functor提供的组件函数。任何实例只需要实现抽象函数point和bind就可以成为Monad实例,然后就可以使用Monad所有的组件函数了。

Monad所提供的主要注入方法(injected method)是在BindOps和MonadOps里。在BindOps里主要提供了flatMap: scalaz/syntax/BindSyntax.scala

final class BindOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Bind[F]) extends Ops[F[A]] {
  ////
  import Liskov.<~<, Leibniz.===

  def flatMap[B](f: A => F[B]) = F.bind(self)(f)

  def >>=[B](f: A => F[B]) = F.bind(self)(f)

  def ∗[B](f: A => F[B]) = F.bind(self)(f)
...

主要是这个flatMap函数,在scalaz里用>>=来表示。这是一个大家都起码耳熟的函数:好像flatMap就代表了Monad。在MonadOps里提供的注入方法如下:scalaz/Syntax/MonadSyntax.scala

final class MonadOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Monad[F]) extends Ops[F[A]] {
  ////

  def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self)

  def whileM[G[_]](p: F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.whileM(p, self)

  def whileM_(p: F[Boolean]): F[Unit] = F.whileM_(p, self)

  def untilM[G[_]](p: => F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.untilM(self, p)

  def untilM_(p: => F[Boolean]): F[Unit] = F.untilM_(self, p)

  def iterateWhile(p: A => Boolean): F[A] = F.iterateWhile(self)(p)

  def iterateUntil(p: A => Boolean): F[A] = F.iterateUntil(self)(p)

  ////
}

看起来这些注入方法都是一些编程语言里的流程控制语法(control flow syntax)。这是不是暗示着Monad最终会实现某种编程语言?我们把这些函数的使用方法放在后面的一些讨论去。我们先来分析一下flatMap函数,因为这是个Monad代表函数。下面是Functor,Applicative和Monad施用函数格式比较:

1 // Functor    :  map[A,B]    (F[A])(f:   A => B):  F[B]
2 // Applicative:  ap[A,B]     (F[A])(f: F[A => B]): F[B]
3 // Monad      :  flatMap[A,B](F[A])(f: A => F[B]): F[B]

以上三种函数款式基本上是一致的。大家都说这就是三种FP的函数施用方式:在一个容器内进行函数的运算后把结果还留在容器内、得到的效果是这样的:F[A] => F[B]。只是它们分别用不同的方式提供这个施用的函数。Functor的map提供了普通函数,Applicative通过容器提供了施用函数ap而Monad则是通过直接函数施用方式来实现F[A] => F[B]: 直接对输入A进行函数施用并产生一个F[B]结果。Monad的这种方式应该不是严格意义上的在容器内进行函数施用。从另一个角度分析,Monad可以被视作某种算法(computation)。Monad F[A]代表了对一个A类型数据的算法(computation)。如果这样说那么Monad就有了全新的解释:Monad就是一种可以对某种类型的数据值进行连续计算的算法(computation):如果我们把flatMap串联起来的话就会是这样的:

1 //   fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))

在这里fa,fb,fc都是F[T]这样的算法。可以看出当我们把flatMap串接起来后就形成了一个串型(sequencial)流程(workflow)的F[]算法。为了更清楚的了解串接flatMap的意义,我们用同等的for-comprehension来示范:

1 //   for {
2 //      a <- (fa: F[A])
3 //      b <- (fb: F[A])
4 //      c <- (fc: F[A])
5 //   } yield { ... }

这样表达会更加清晰了:我们先运算fa,得到结果a后接着运算fb,得出结果b后再运算fc,得出结果c ... 这像是一段行令程序(imperative program)。我们再用个形象点的例子来示范说明:

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Bar { def baz: Option[Baz] }

def compute(maybeFoo: Option[Foo]): Option[Int] =
 maybeFoo.flatMap { foo =>
  foo.bar.flatMap { bar =>
    bar.baz.map { baz =>
      baz.compute
    }
  }
 }
def compute2(maybeFoo: Option[Foo]): Option[Int] =
  for {
      foo <- maybeFoo
      bar <- foo.bar
      baz <- bar.baz
  }  yield baz.compute

可以看出,每一个算法都依赖前面算法得出的结果。从这个例子我们可以得出Monad的串型运算(sequencial computation)特性。确切来说,flatMap并不适合并行运算,所以我们需要Applicative。这是因为Applicative是在既有的容器中运算,而flatMap则会重新创建新的容器(在Monad的世界里容器即为算法(computation)。但是因为我们讲过Monad就是Applicative,所以Monad也可以实现并行运算。Applicative 的 ap 函数可以用 flatMap实现:

1 // ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))  

也可以用flatMap来实现Functor的map函数:

1 // map[A,B](fa: F[A])(f: A => B): F[B] = fa.flatMap(a => point(f(a)))  

从上面的例子好像可以领悟一些关于FP即Monadic Programming的说法。形象的来讲:这个所谓的算法Monad F[]就好像是在F[]这么个壳子里进行传统编程:还记着的话,FP编程既是纯函数(pure function)对F[T]里的T值进行运算,没有中间变量(temp variable),没有副作用(no side-effect)。但现在有了Monad,我们就可以使用传统的行令编程(imperative programming)了。再形象一点来说上面的for loop就像F[]壳子,在for loop内可以进行申明变量,更新状态等OOP式行令编程。但这些变化(mutability)不会漏出for loop之外。不过,本篇所述Monad编程的单一局限性还是很明显的:因为在for loop 内部的操作函数都必须返回同一种类型的Monad实例如:Option[], List[],SomeType[]等等。而且程序运算行为只会受一种类型的特性所控制。如上面所叙,Monad实例的类型控制Monadic程序的运算行为。每一种Monad实例的程序可以有不同的运算方式。如果需要多种类型行为的Monad程序,就需要使用Monad Transformer typeclass了。这个在将来的讨论中自会提及,现在好像说的过头了。我们还是回到Monad的基本操作。

Option是scala标准库的一个类型。它已经是个Monad,所以可以使用flatMap:

1 2.some flatMap {x => (x + 3).some }               //> res0: Option[Int] = Some(5)
2 2.some >>= { x => (x + 3).some }                  //> res1: Option[Int] = Some(5)
3 (none: Option[Int]) >>= {x => (x + 3).some }      //> res2: Option[Int] = None

我们可以用Monad[T] point来把一个普通值A升格到T[A]:

1 Monad[Option].point(2)                            //> res3: Option[Int] = Some(2)
2 Monad[Option].point(2) >>= {x => Monad[Option].point(x + 3)}
3                                                   //> res4: Option[Int] = Some(5)
4 (None: Option[Int]) >>= {x => Monad[Option].point(x + 3)}
5                                                   //> res5: Option[Int] = None

在上面的例子里我们不断提及Option Monad是有原因的,因为Option类型的Monad典型实例,在控制运算流程时最有特点:可以在中途退出,在遇到None值时可以立即终止运算。

我们用一个比较现实点的例子来示范:我正尝试用自己的方式来练习举重 - 我最多能举起50KG、每个杠铃片重2.5公斤、杠铃两端不必平衡,但一边不得超过另一边多于3个杠铃片(多3个还没问题)。试着用一个自定义类型来模拟举重:

type Discs = Int  //杠铃片数量
case class Barbell(left: Discs, right: Discs) {
    def loadLeft(n: Discs): Barbell = copy(left = left + n)
    def loadRight(n: Discs): Barbell = copy(right = right + n)
}
Barbell(0,0).loadLeft(1)                          //> res8: Exercises.monad.Barbell = Barbell(1,0)
Barbell(1,0).loadRight(1)                         //> res9: Exercises.monad.Barbell = Barbell(1,1)
Barbell(2,1).loadLeft(-1)                         //> res10: Exercises.monad.Barbell = Barbell(1,1)

现在这个自定义类型Barbell是可以跟踪当前杠铃左右重量状态的。现在我把往杠铃上增加重量片的过程串联起来:

1 Barbell(0,0).loadLeft(1).loadRight(2).loadRight(100).loadLeft(2).loadRight(-99)
2                                                   //> res11: Exercises.monad.Barbell = Barbell(3,3)

可以看到这个过程中有些环节已经超出了我的能力,但杠铃最终状态好像还是合理的。我们需要在重量配置不合理的时候就立即终止。现在我们可以用Option来实现这项功能:

type Discs = Int  //杠铃片数量
case class Barbell(left: Discs, right: Discs) {
  def loadLeft(n: Discs): Option[Barbell] = copy(left = left + n) match {
    case Barbell(left,right) => if ( (left+right <= 20) && math.abs(left-right) <=3 ) Some(Barbell(left,right)) else None
    case _ => None
  }
  def loadRight(n: Discs): Option[Barbell] = copy(right = right + n) match {
    case Barbell(left,right) => if ( (left+right <= 20) && math.abs(left-right) <=3 ) Some(Barbell(left,right)) else None
    case _ => None
  }
}
Barbell(0,0).loadLeft(1)                          //> res8: Option[Exercises.monad.Barbell] = Some(Barbell(1,0))
Barbell(1,0).loadRight(1)                         //> res9: Option[Exercises.monad.Barbell] = Some(Barbell(1,1))
Barbell(2,1).loadLeft(-1)                         //> res10: Option[Exercises.monad.Barbell] = Some(Barbell(1,1))
Barbell(0,0).loadLeft(4)                          //> res11: Option[Exercises.monad.Barbell] = None
Barbell(15,1).loadRight(15)                       //> res12: Option[Exercises.monad.Barbell] = None

超出重量平衡的情况返回了None。现在返回值是个Option,而Option是个Monad,所以我们可以用flatMap把每个环节串联起来:

Barbell(0,0).loadLeft(3) >>= {_.loadRight(3)}     //> res13: Option[Exercises.monad.Barbell] = Some(Barbell(3,3))
Barbell(0,0).loadLeft(3) >>= {_.loadRight(3) >>= {_.loadRight(1)}}
                                                  //> res14: Option[Exercises.monad.Barbell] = Some(Barbell(3,4))
Barbell(0,0).loadLeft(3) >>= {_.loadRight(3) >>= {_.loadRight(1) >>= {_.loadLeft(4)}}}
                                                  //> res15: Option[Exercises.monad.Barbell] = Some(Barbell(7,4))
Barbell(0,0).loadLeft(1) >>= {_.loadRight(5) >>= {_.loadLeft(2)}}
                                                  //> res16: Option[Exercises.monad.Barbell] = None
Monad[Option].point(Barbell(0,0)) >>= {_.loadLeft(3) >>= {_.loadRight(6)}}
                                                  //> res17: Option[Exercises.monad.Barbell] = Some(Barbell(3,6))

我们的最终目的是用for-comprehension来表述,会更加清晰:

def addWeight: Option[Barbell] = for {
    b0 <- Monad[Option].point(Barbell(0,0))
    b1 <- b0.loadLeft(3)
    b2 <- b1.loadRight(3)
} yield b2                                        //> addWeight: => Option[Exercises.monad.Barbell]
addWeight                                         //> res18: Option[Exercises.monad.Barbell] = Some(Barbell(3,3))

def addWeight1: Option[Barbell] = for {
    b0 <- Monad[Option].point(Barbell(0,0))
    b1 <- b0.loadLeft(4)
    b2 <- b1.loadRight(3)
} yield b2                                        //> addWeight1: => Option[Exercises.monad.Barbell]
addWeight1                                        //> res19: Option[Exercises.monad.Barbell] = None

 

从以上的例子可以得出:实现了一个数据类型的Monad实例后就可以获取以这个类型控制运算行为的一种简单的编程语言,这种编程语言可以在for loop内部实现传统的行令编程风格。

在本篇讨论中我们介绍了Monad实际上是一种编程模式,并且示范了简单的for loop内部流程运算。在下面的一系列讨论中我们将会了解更多类型的Monad,以及Monad如何能成为功能完善的编程语言。

时间: 2024-08-20 13:18:20

Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern的相关文章

Python中的函数式编程

虽然人们总把Python当作过程化的,面向对象的语言,但是他实际上包含了函数化编程中,你需要的任何东西.这篇文章主要讨论函数化编程的一般概念,并说明用Python来函数化编程的技术. 我们最好从艰难的问题开始出发:"到底什么是函数化编程呢?"其中一个答案可能是这样的,函数化编程就是你在使用Lisp这样的语言时所做的(还有Scheme,Haskell,ML,OCAML,Mercury,Erlang和其他一些语言).这是一个保险的回答,但是它解释得并不清晰.不幸的是对于什么是函数化编程,很

为什么用 JavaScript 学习函数式编程?(软件编写)(第二部分)

本文讲的是为什么用 JavaScript 学习函数式编程?(软件编写)(第二部分), 烟雾的方块艺术 -MattysFlicks -(CC BY 2.0) 注意:这是从基础学习函数式编程和使用 JavaScript ES6+ 撰写软件的第二部分.保持关注,接下来还有很多!第一篇 | 第三篇 > 忘掉你认为知道的关于 JavaScript 的一切,用初学者的眼光去看待它.为了帮助你做到这一点,我们将会从头复习一下 JavaScript 的基础,就像你与其尚未谋面一样.如果你是初学者,那你就很幸运了

准备充分了嘛就想学函数式编程?(第一部分)

本文讲的是准备充分了嘛就想学函数式编程?(第一部分), 迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步.但是不一定特别难.只要选对了思考方法就不难. 学开车 第一次学车时,我们也曾挣扎过.看别人学开车时觉得真的很简单.但事实上学车比我们想象的难多了. 我们借父母的车子练习,在家周围街道上开熟练之前甚至都不敢冒险开到公路上去. 但是通过不断的练习,在经历过一些父母想忘掉的担心令人的经历之后,我们学会了开车,最终拿到了驾照. 拿到驾照之后我们一有机会就会把车开出去.每次出行都会让我们的

详细Python修饰器Decorator的函数式编程【荐】

Python 的 Decorator在使用上和Java/C#的Annotation很相似,就是在方法名前面加一个@XXX注解来为这个方法装饰一些东西.但是,Java/C#的Annotation也很让人望而却步,太TMD的复杂了,你要玩它,你需要了解一堆Annotation的类库文档,让人感觉就是在学另外一门语言. 而Python使用了一种相对于Decorator Pattern和Annotation来说非常优雅的方法,这种方法不需要你去掌握什么复杂的OO模型或是Annotation的各种类库规定

Python装饰器的函数式编程详解_python

Python的装饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都很相似--都是想要对一个已有的模块做一些"修饰工作",所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去.但是OO的Decorator简直就是一场恶梦,不信你就去看看wikipedia上的词条

[译] JavaScript 的函数式编程是一种反模式

本文讲的是[译] JavaScript 的函数式编程是一种反模式, 原文地址:Functional programming in JavaScript is an antipattern 原文作者:Alex Dixon 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:sunui 校对者:LeviDing.xekri 其实 Clojure 更简单些 写了几个月 Clojure 之后我再次开始写 JavaScript.就在我试着写一些很普通的东西的时候,我

函数式编程入门教程

你可能听说过函数式编程(Functional programming),甚至已经使用了一段时间. 但是,你能说清楚,它到底是什么吗? 网上搜索一下,你会轻松找到好多答案. 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式. 最主要的特征是,函数是第一等公民. 强调将计算过程分解成可复用的函数,典型例子就是map方法和reduce方法组合而成 MapReduce 算法. 只有纯的.没有副作用的函数,才

函数式编程很难,这正是你要学习它的原因

很奇怪不是,很少有人每天都使用函数式编程语言.如果你用Scala,Haskell,Erlang,F#或某个Lisp方言来编程,很可能没有公司会花钱聘你.这个行业里的绝大部分人都是使用像Python,Ruby,Java或C#等面向对象的编程语言--它们用起来很顺手.不错,你也许会偶然用到一两个"函数式语言特征",例如"block",但人们不会去做函数式编程. 然而,很多年来,我们一直被教导说函数式编程语言很好很棒.我仍然记得当我第一次阅读ESR的著名的关于学习Lisp

给 JavaScript 开发者讲讲函数式编程

和大多数人一样,我在几个月前听到了很多关于函数式编程的东西,不过并没有更深入的了解.于我而言,可能只是一个流行词罢了.从那时起,我开始更深地了解函数式编程并且我觉得应该为那些总能听到它但不知道究竟是什么的新人做一点事情. 谈及函数式编程,你可能会想到它们:Haskell 和 Lisp,以及很多关于哪个更好的讨论.尽管它们都是函数式语言,不过的确有很大的不同,可以说各有各的卖点.在文章的结尾处,我希望你能够对这些有一个更加清晰的认识.它们都在某些更加现代的语言上留下了自己的影子.你可能听说过这样两