Scalaz(12)- Monad:再述述flatMap,顺便了解MonadPlus
在前面的几篇讨论里我们初步对FP有了些少了解:FP嘛,不就是F[A]吗?也是,FP就是在F[]壳子(context)内对程序的状态进行更改,也就是在F壳子(context)内施用一些函数。再直白一点就是在F壳子内进行OOP惯用的行令编程(imperative programming)。当然,既然是在壳子(context)内进行编程这种新的模式,那么总需要些新的函数施用方法吧。我们再次审视一下以前了解过的FP函数施用方法:
// Functor : map[A,B] (F[A])(f: A => B): F[B]
// Applicative: ap[A,B] (F[A])(f: F[A => B]): F[B]
// Monad : flatMap[A,B](F[A])(f: A => F[B]): F[B]
它们分别代表了scalaz的三个typeclass。对于FP编程来讲,函数施用(function application)就是改变程序状态,也就是map。那么从map角度分析,如果直接对F[A=>B], A=>F[B]进行map会产生不同的结果类型,如直接map A=>F[B]结果是F[F[B]]。所以我们会想办法把结果类型对齐了,使最终结果类型为F[B]:
def ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))
def flatMapByJoin[A,B](ma: M[A])(f: A => M[B]): M[B] = join(map(ma)(a => f(a)))
def join[A](mma: M[M[A]]): M[A]
从上面的代码中我们看到:在flatMap我们通过join施用了map。而这个join好像就是为了把F[F[B]]打平到F[B]而设计的,这点从join函数款式(signature)可以看出。难道FP就是为了实现类型匹配吗?绝不是!我们不能把眼光局限在如何取得类型匹配上,而是应该放阔到函数施用的目的上。我们从上面函数map,ap,flatMap的类型款式可以看出:map,ap都是在F[]壳(context)内施用的,而flatMap是在壳外对输入的类型A值进行施用的,但把结果放入了壳内。这可以说是flatMap与map,ap的根本不同之处。那么flatMap代表着什么呢?如果从flatMap的函数款式(function signature)分析:它是一个递归算法: 给F[A]一个A产生F[B],再给F[B]一个B再产生F[C]...如此类推。这样看来flatMap是一个持续算法(computational continuation),如果把flatMap串联起来就可以实现某种编程语法(syntax)。这个推论在scala的for-comprehension中得到证实:flatMap可以被视作一种简单的FP语法,它使我们可以在for-comprehension中使用我们熟悉的行令编程,其结果是FP模式的行令编程。flatMap是Monad的标识函数,而Monad又具备所有的FP函数施用方法因为它继承了Functor和Applicative,所以有些人把FP编程称为Monadic programming。从这里也可以看到flatMap在FP编程里的重要性。
如果从flatMap代表持续算法这个角度分析:flatMap实际连接了两个算法F[A] => F[B]。我们应该可以在运算flatMap的过程中实现一些附加的效果。这个要求应该可以在实现flatMap函数时做到。我们这篇讨论的重点就是在示范如何在实现flatMap时增加一些效果。当把一串算法用flatMap链接起来时这些附加效果是如何积累的。
我想没什么比logger更能示范串接算法前面算法的一些效果是如何流转到下面的算法里的。我们来设计一个例子:模拟一个输入装置,每接收一次输入代表一次运算,用一个logger把每次运算的输入都记录下来。当然,这个例子用State Monad就很容易实现。不过我们的目的是去示范如何通过flatMap把效果传递下去的,所以还是应该紧贴着如何实现flatMap:
trait KeyLog[K] {
def value: K
def log: String
override def toString = "["+value+","+log+"]"
}
object KeyLog {
def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] {
def value = k
def log = msg
}
} KeyLog(,"Entered Number 3") //> res0: Exercises.keylog.KeyLog[Int] = [3,Entered Number 3]
KeyLog("Hello", "Entered String 'Hello'") //> res1: Exercises.keylog.KeyLog[String] = [Hello,Entered String 'Hello']
我们用KeyLog[K]来代表这个输入算法。每个算法都包含一个K类型的value和String类型的log。对于类型参数K我们可以直接用普通的flatMap K => KeyLog[I]来转变value。而我们的目的是如何通过flatMap把前一个KeyLog的log累积到下个算法的log。挺简单,是吧?在KeyLog结构里转变log并把结果留在KeyLog里,听着像是map,不过map是针对K的。所以我们要先加个mapLog:
trait KeyLog[K] {
def value: K
def log: String
override def toString = "["+value+","+log+"]"
def mapLog(preLog: String): KeyLog[K] = KeyLog(value,preLog +";"+log)
}
我们试着实现flatMap:
trait KeyLog[K] {
def value: K
def log: String
override def toString = "["+value+","+log+"]"
def mapLog(preLog: String): KeyLog[K] = KeyLog(value,preLog +";"+log)
def flatMap[I](f: K => KeyLog[I]): KeyLog[I] =
f(value).mapLog(log)
}
确实简单又直接:f(value) 产生 KeyLog[I] 然后在这个接着的算法中调用 mapLog 把上一个算法KeyLog[K]的log并入KeyLog[I]的log。
我们试着用一下flatMap
KeyLog(,"Entered Number 3").flatMap(a => KeyLog("Hello", "Entered String 'Hello'"))
//> res2: Exercises.keylog.KeyLog[String] = [Hello,Entered Number 3;Entered Stri
//| ng 'Hello']
最终log值:"Entered Number 3;Entered String 'Hello'。我们实现了在运算flatMap过程中对log进行的累积。
现在我们可以先获取KeyLog的Monad实例,然后进行flatMap串联及使用for-comprehension进行行令编程了:
object KeyLog {
def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] {
def value = k
def log = msg
}
import scalaz._
import Scalaz._
implicit object keylogMonad extends Monad[KeyLog] {
def point[K](k: => K): KeyLog[K] = KeyLog(k,"")
def bind[K,I](kk: KeyLog[K])(f: K => KeyLog[I]): KeyLog[I] = kk flatMap f
}
}
在KeyLog Monad实例里bind使用了我们设计的flatMap函数。看看flatMap串接和for-comprehension效果:
def enterInt(k: Int): KeyLog[Int] = KeyLog(k, "Number:"+k.toString)
//> enterInt: (k: Int)Exercises.keylog.KeyLog[Int]
def enterStr(k: String): KeyLog[String] = KeyLog(k,"String:"+k)
//> enterStr: (k: String)Exercises.keylog.KeyLog[String]
enterInt() >>= {a => enterInt() >>= {b => enterStr("Result:") map {c => c + (a * b).toString} }}
//> res3: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String:
//| Result:;]
for {
a <- enterInt()
b <- enterInt()
c <- enterStr("Result:")
} yield c + (a * b).toString //> res4: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String
//| :Result:;]
value和log都按照要求实现了转变。
在使用for-comprehension时突然想到守卫函数(guard function)。我想既然已经得到了KeyLog的Monad实例,是不是可以在它的for-comprehension里使用守卫函数呢?就像这样:
for {
a <- enterInt()
b <- enterInt() if b >
c <- enterStr("Result:")
} yield c + (a * b).toString
不过无法通过编译。提示需要filter函数。查了一下MonadPlus typeclass可以提供这个函数。那么我们就沿着惯用的套路获取一下KeyLog的MonadPlus实例。MonadPlus trait的定义如下:scalaz/MonadPlus.scala
trait MonadPlus[F[_]] extends Monad[F] with ApplicativePlus[F] { self =>
//// /** Remove `f`-failing `A`s in `fa`, by which we mean: in the
* expression `filter(filter(fa)(f))(g)`, `g` will never be invoked
* for any `a` where `f(a)` returns false.
*/
def filter[A](fa: F[A])(f: A => Boolean) =
bind(fa)(a => if (f(a)) point(a) else empty[A])
...
MonadPlus又继承了ApplicativePlus:scalar/ApplicativePlus.scala
trait ApplicativePlus[F[_]] extends Applicative[F] with PlusEmpty[F] { self =>
ApplicativePlus又继承了PlusEmpty: scalaz/PlusEmpty.scala
trait PlusEmpty[F[_]] extends Plus[F] { self =>
////
def empty[A]: F[A]
...
PlusEmpty定义了抽象成员empty[A],又继承了Plus: scalar/Plus.scala
trait Plus[F[_]] { self =>
//// /**The composition of Plus `F` and `G`, `[x]F[G[x]]`, is a Plus */
def compose[G[_]](implicit G0: Plus[G]): Plus[({type λ[α] = F[G[α]]})#λ] = new CompositionPlus[F, G] {
implicit def F = self implicit def G = G0
} /**The product of Plus `F` and `G`, `[x](F[x], G[x]])`, is a Plus */
def product[G[_]](implicit G0: Plus[G]): Plus[({type λ[α] = (F[α], G[α])})#λ] = new ProductPlus[F, G] {
implicit def F = self implicit def G = G0
} def plus[A](a: F[A], b: => F[A]): F[A] def semigroup[A]: Semigroup[F[A]] = new Semigroup[F[A]] {
def append(f1: F[A], f2: => F[A]): F[A] = plus(f1, f2)
}
...
Plus又定义了抽象成员plus[A],那么获取MonadPlus实例必须实现empty[A]: F[A]和plus[A](a: F[A], b: F[A]): F[A]。看来这个PlusEmpty就是一种Monoid,只不过是针对高阶类型的。我们知道Monad实例类型必须是高阶的M[_],那么如果Monad实例同时又具备Monoid特性的话,那么就可以使用MonadPlus来描述它的性质。
好了,现在我想获取KeyLog[K]的MonadPlu实例,那么我必须实现empty[A]:F[A]和plus[A](a:F[A],b:F[A]):F[A]。KeyLog[K]的empty[K]是什么呢?想了半天没得到答案,可能KeyLog[K]就没有empty[K]吧。也许我们想取得KeyLog MonadPlu实例的目的还没搞清楚。看看上面的需求:
for {
a <- enterInt()
b <- enterInt() if b >
c <- enterStr("Result:")
} yield c + (a * b).toString
从字面上看是希望通过守卫函数过滤数字为0的数字。等等,enterInt(4)已经确定了输入为4,是 > 0,还过滤什么?不是找事吗。所以我们的目的应该聚焦在过滤需求上。Scalaz为List,Option提供了MonadPlus实例,我们看看这两种类型的守卫函数使用:
for { //list
a <- |-> if a.shows contains ''
} yield a //> res5: List[Int] = List(7, 17, 27, 37, 47)
for { //option
a <- Some()
b <- Some() if a < b
} yield b //> res6: Option[Int] = Some(4)
for { //option
a <- Some()
b <- Some() if a > b
} yield b //> res7: Option[Int] = None
先来分析List例子:一个List可能是空的,又可能有多过一个元素,有多种可能。守卫函数的功能就是在这些可能里进行选择。
再分析Option:可能是None或者Some,这本身就是一种筛选。对于KeyLog[K],它只有一种状态,没有选择的需要,所以我无法实现KeyLog[K]的empty[K]。
List和Option的empty分别是:Nil和None,这个很容易理解。那么plus呢?把 plus(list1,list2):list3 = list1 ++ list2这个倒是容易理解,但plus(option1,option2):option3这个又应该怎么理解呢?我们还是看看在scalaz里是怎么定义plus的吧:scalaz.std/List.scala
implicit val listInstance = new Traverse[List] with MonadPlus[List] with Each[List] with Index[List] with Length[List] with Zip[List] with Unzip[List] with Align[List] with IsEmpty[List] with Cobind[List] {
...
def empty[A] = Nil
def plus[A](a: List[A], b: => List[A]) = a ++ b
...
List的plus就是把两个List接起来(concat)
scalaz.std/Option.scala
implicit val optionInstance = new Traverse[Option] with MonadPlus[Option] with Each[Option] with Index[Option] with Length[Option] with Cozip[Option] with Zip[Option] with Unzip[Option] with Align[Option] with IsEmpty[Option] with Cobind[Option] with Optional[Option] {
...
def empty[A]: Option[A] = None
def plus[A](a: Option[A], b: => Option[A]) = a orElse b
...
Option的plus意思是如果a是None就取b否则取a,无论b是否None。我们用MonadPlus提供的操作符号<+>来示范:
List(,,) <+> List(,,) //> res4: List[Int] = List(1, 2, 3, 4, 5, 6)
Nil <+> List(,,) //> res5: List[Int] = List(1, 2, 3)
List(,,) <+> Nil //> res6: List[Int] = List(1, 2, 3)
none <+> .some //> res7: Option[Int] = Some(2)
.some <+> .some //> res8: Option[Int] = Some(2)
.some <+> none //> res9: Option[Int] = Some(2)
none <+> none //> res10: Option[Nothing] = None
为了实现KeyLog MonadPlus实例,我们必须对KeyLog类型重新定义使之包含多过一种状态:
import scalaz._
import Scalaz._
object keylog {
trait KeyLog[+K] {
override def toString = this match {
case KeyIn(value,log) => "["+value+","+log+"]"
case _ => "[Keypad Locked]"
}
def mapLog(preLog: String): KeyLog[K] = this match {
case KeyIn(value,log) => KeyIn(value,preLog +";"+log)
case _ => KeyLock
}
def flatMap[I](f: K => KeyLog[I]): KeyLog[I] = this match {
case KeyIn(value,log) => f(value).mapLog(log)
case _ => KeyLock
}
}
case class KeyIn[K](value: K, log: String) extends KeyLog[K]
case object KeyLock extends KeyLog[Nothing]
object KeyLog {
/* def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] {
def value = k
def log = msg
} */
implicit object keylogMonad extends Monad[KeyLog] {
def point[K](k: => K): KeyLog[K] = KeyIn(k,"")
def bind[K,I](kk: KeyLog[K])(f: K => KeyLog[I]): KeyLog[I] = kk flatMap f
}
}
我们增加了KeyIn和KeyLock两种状态。然后我们只需要通过模式匹配(pattern matching)在实现前面逻辑的时候把多种KeyLog状态考虑进去。
运行前面的例子:
def enterInt(k: Int): KeyLog[Int] = KeyIn(k, "Number:"+k.toString)
//> enterInt: (k: Int)Exercises.keylog.KeyLog[Int]
def enterStr(k: String): KeyLog[String] = KeyIn(k,"String:"+k)
//> enterStr: (k: String)Exercises.keylog.KeyLog[String]
enterInt() >>= {a => enterInt() >>= {b => enterStr("Result:") map {c => c + (a * b).toString} }}
//> res0: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String
//| :Result:;]
for {
a <- enterInt()
b <- enterInt()
c <- enterStr("Result:")
} yield c + (a * b).toString //> res1: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String
//| :Result:;]
现在把KeyLock效果加进去:
enterInt() >>= {a => (KeyLock: KeyLog[Int]) >>= {b => enterStr("Result:") map {c => c + (a * b).toString} }}
//> res2: Exercises.keylog.KeyLog[String] = [Keypad Locked]
for {
a <- enterInt()
b <- enterInt()
x <- (KeyLock: KeyLog[String])
c <- enterStr("Result:")
} yield c + (a * b).toString //> res3: Exercises.keylog.KeyLog[String] = [Keypad Locked]
正是我们期待的效果。
现在我们可以把MonadPlus特质混入keylogMonad实例(trait mix-in):
implicit object keylogMonad extends Monad[KeyLog] with MonadPlus[KeyLog] {
def point[K](k: => K): KeyLog[K] = KeyIn(k,"")
def bind[K,I](kk: KeyLog[K])(f: K => KeyLog[I]): KeyLog[I] = kk flatMap f def empty[K]: KeyLog[K] = KeyLock
def plus[K](a: KeyLog[K], b: => KeyLog[K]): KeyLog[K] = a match {
case KeyIn(value,log) => KeyIn(value,log)
case KeyLock => b
}
}
在实例中我们实现了empty和plus。
那么现在我们可以使用守卫函数了吧:
for {
a <- enterInt()
b <- enterInt()
c <- enterStr("Result:")
} yield c + (a * b).toString //> res3: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String
//| :Result:;]
for {
a <- enterInt()
b <- enterInt() if b >
c <- enterStr("Result:")
} yield c + (a * b).toString //> res4: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;;Strin
//| g:Result:;]
for {
a <- enterInt()
b <- enterInt() if b >
c <- enterStr("Result:")
} yield c + (a * b).toString //> res5: Exercises.keylog.KeyLog[String] = [Keypad Locked]
守卫函数按要求对KeyLog状态进行了过滤。
Scalaz(12)- Monad:再述述flatMap,顺便了解MonadPlus的更多相关文章
- Scalaz(11)- Monad:你存在的意义
前面提到了scalaz是个函数式编程(FP)工具库.它提供了许多新的数据类型.拓展的标准类型及完整的一套typeclass来支持scala语言的函数式编程模式.我们知道:对于任何类型,我们只需要实现这 ...
- Scalaz(41)- Free :IO Monad-Free特定版本的FP语法
我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码.像通过键盘显示器进行交流.读写文件.数据库等这些IO操作都会产生副作用.那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段 ...
- Scalaz(17)- Monad:泛函状态类型-State Monad
我们经常提到函数式编程就是F[T].这个F可以被视为一种运算模式.我们是在F运算模式的壳子内对T进行计算.理论上来讲,函数式程序的运行状态也应该是在这个运算模式壳子内的,也是在F[]内更新的.那么我们 ...
- Scalaz(44)- concurrency :scalaz Future,尚不完整的多线程类型
scala已经配备了自身的Future类.我们先举个例子来了解scala Future的具体操作: import scala.concurrent._ import ExecutionContext. ...
- Scalaz(35)- Free :运算-Trampoline,say NO to StackOverflowError
在前面几次讨论中我们介绍了Free是个产生Monad的最基本结构.它的原理是把一段程序(AST)一连串的运算指令(ADT)转化成数据结构存放在内存里,这个过程是个独立的功能描述过程.然后另一个独立运算 ...
- Scalaz(34)- Free :算法-Interpretation
我们说过自由数据结构(free structures)是表达数据类型的最简单结构.List[A]是个数据结构,它是生成A类型Monoid的最简单结构,因为我们可以用List的状态cons和Nil来分别 ...
- Promise是Monad吗?
译者按: 近年来,函数式语言的特性都被其它语言学过去了. 原文: Functional Computational Thinking — What is a monad? 译者: Fundebug 为 ...
- 函数式JS: 原来promise是这样的monad
转载请注明出处: http://hai.li/2017/03/27/prom... 背景 上篇文章 函数式JS: 一种continuation monad推导 得到了一个类似promise的链式调用, ...
- 十一、从头到尾彻底解析Hash 表算法
在研究MonetDB时深入的学习了hash算法,看了作者的文章很有感触,所以转发,希望能够使更多人受益! 十一.从头到尾彻底解析Hash 表算法 作者:July.wuliming.pkuoliver ...
随机推荐
- atitit 点播系统 概览 v2 qb1.docx
atitit 点播系统 概览 v2 qb1.docx 1.1. 多界面(可以挂载多个不同的界面主题)1 1.2. 独立的选片模块(跨设备,跨平台)2 1.3. 跨设备平台(android安卓盒子,pc ...
- Android 4.2版本以下使用WebView组件addJavascriptInterface方法存在JS漏洞
JS注入漏洞存在的Android版本:Android < 4.2 综述:Android的SDK中提供了一个WebView组件,用于在应用中嵌入一个浏览器来进行网页浏览.WebView组件中的ad ...
- viewpage listview gridview加载本地大图多图OOM处理办法
很少上博客园写东西了. 最近在写公司项目,由于需要加载本地相册通过viewpager方式来加载, 最后发现直接进入界面就OOM了. 经过几天的整理最终搞定. 现在将加载本地和加载网络图片的缓存工具类贴 ...
- Mybatis中SqlMapper配置的扩展与应用(1)
奋斗了好几个晚上调试程序,写了好几篇博客,终于建立起了Mybatis配置的扩展机制.虽然扩展机制是重要的,然而如果没有真正实用的扩展功能,那也至少是不那么鼓舞人心的,这篇博客就来举几个扩展的例子. 这 ...
- CSS背景100%平铺 浏览器缩小背景显示不全解决办法
本文我们分享前端CSS背景100%平铺,浏览器缩小背景显示不全bug解决的两个方法,如果你也遇到了,那么就可以参考下面文章. 把浏览器的窗口缩小时,拖动滚动条时你会发现原本设定的CSS背景100%平铺 ...
- C++中typename关键字的用法
我在我的 薛途的博客 上发表了新的文章,欢迎各位批评指正. C++中typename关键字的用法
- 一个在ASP.NET中利用服务器控件GridView实现数据增删改查的例子
备注:这是我辅导的一个项目开发组的一个例子,用文章的方式分享出来,给更多的朋友参考.其实我们这几年的项目中,都不怎么使用服务器控件的形式了,而是更多的采用MVC这种开发模式.但是,如果项目的历史背景是 ...
- Kruskal算法(三)之 Java详解
前面分别通过C和C++实现了克鲁斯卡尔,本文介绍克鲁斯卡尔的Java实现. 目录 1. 最小生成树 2. 克鲁斯卡尔算法介绍 3. 克鲁斯卡尔算法图解 4. 克鲁斯卡尔算法分析 5. 克鲁斯卡尔算法的 ...
- IOS开发初步
由于工程实践项目的原因,得学习下IOS开发,今天才知道苹果09年才出的开发工具和开发包,也就是说,满打满算,现在顶多有5年IOS开发的工作经验.在我国2010年才火起来,因为那时候国内的iphone4 ...
- sublime 插件总结
sublime的强大之处在于其丰富的插件,记录一下常用的插件. 1.Color Highlighter(识别代码中的颜色) 默认如下显示 做如下修改,打开插件默认设置,并复制到用户设置,将 " ...