在前面的几篇讨论里我们初步对FP有了些少了解:FP嘛,不就是F[A]吗?也是,FP就是在F[]壳子(context)内对程序的状态进行更改,也就是在F壳子(context)内施用一些函数。再直白一点就是在F壳子内进行OOP惯用的行令编程(imperative programming)。当然,既然是在壳子(context)内进行编程这种新的模式,那么总需要些新的函数施用方法吧。我们再次审视一下以前了解过的FP函数施用方法:
1 1 // Functor : map[A,B] (F[A])(f: A => B): F[B]
2 2 // Applicative: ap[A,B] (F[A])(f: F[A => B]): F[B]
3 3 // 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]:
1 def ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a))) 2 def flatMapByJoin[A,B](ma: M[A])(f: A => M[B]): M[B] = join(map(ma)(a => f(a))) 3 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:
1 trait KeyLog[K] { 2 def value: K 3 def log: String 4 override def toString = "["+value+","+log+"]"
5 } 6 object KeyLog { 7 def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] { 8 def value = k 9 def log = msg 10 } 11 } 12
13 KeyLog(3,"Entered Number 3") //> res0: Exercises.keylog.KeyLog[Int] = [3,Entered Number 3]
14 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:
1 trait KeyLog[K] { 2 def value: K 3 def log: String 4 override def toString = "["+value+","+log+"]"
5 def mapLog(preLog: String): KeyLog[K] = KeyLog(value,preLog +";"+log) 6 }
我们试着实现flatMap:
1 trait KeyLog[K] { 2 def value: K 3 def log: String 4 override def toString = "["+value+","+log+"]"
5 def mapLog(preLog: String): KeyLog[K] = KeyLog(value,preLog +";"+log) 6 def flatMap[I](f: K => KeyLog[I]): KeyLog[I] =
7 f(value).mapLog(log) 8 }
确实简单又直接:f(value) 产生 KeyLog[I] 然后在这个接着的算法中调用 mapLog 把上一个算法KeyLog[K]的log并入KeyLog[I]的log。
我们试着用