@MiloXia
2015-07-20T10:20:54.000000Z
字数 4508
阅读 2269
函数式编程 scala
Free最大的价值在于Monadic for-comprehension和Interpreter,前者只要描述算法,后者随意解释,可类比为接口和实现。
Free 可以将任意functor F[A]变成monad Free[F[_], A]
毕竟函数式编程解决问题的手段都是从低阶类型lift到高阶类型,再套一层Context来表达低阶类型不可计算的问题,类似于封装了一层。
下面举例说明:
假设你上班前有以下动作:
1. 起床 - getUp
2. 洗漱 - wash
3. 做早饭 - makeBreakfast
4. 吃早饭 - eat
5. 出门 - goOut
在scala里面你会很自然的这么写
def getUp() {...}def wash() {...}def makeBreakfast: Breakfast = {...}def eat(breakfast: Breakfast) {....}def goOut() {...}getUp()wash()val breakfast = makeBreakfast()eat(breakfast)goOut()
这是一个非常简单的算法逻辑,但是这段代码有一个很严重的问题!你看到没? 哈哈,其实没有问题,只是这段代码是非纯函数式的,方法调用依次往下,满满的副作用,有木有?
像Haskell这种无";"的语言是不能这样写的,所以在没有";"的情况下你该怎么写?
两种选择:CPS 和 Monad
前者直接忽略,除非你非常喜欢回调,用Monad你会这么写:
val doo = for {_ <- getUp()_ <- wash()breakfast = makeBreakfast()_ <- eat(breakfast)_ <- goOut()} yield ()
但是,编译不过啊!我们没有给我们这个简单算法逻辑抽象出Monad来啊!怎么办? 用Free就可以啊,它可以提供Monad啊,怎么用?
1. 先为我们的算法抽象出一个可以表示的结构(数据结构)出来,而不是上面那样定义一串函数:
case class Breakfast(egg: Int, milk: Int)sealed trait GoToWork[A]case object GetUp extends GoToWork[Unit]case object Wash extends GoToWork[Unit]case object MakeBreakfast extends GoToWork[Breakfast]case class Eat(breakfast: Breakfast) extends GoToWork[Unit]case object GoOut extends GoToWork[Unit]
这里关建的不是把函数变成case class或object, 而是引入了GoToWork[A],我们要用Free, 必须得有F[A]
1.5 显然上面的case class和object之间没有啥联系,我们得引入表示他们关系的结构:
trait DoFree[F[_], A] {def unit(a: A): DoFree[F,A] = Return(a)}final case class Return[F[_], A](a: A) extends DoFree[F,A]final case class Bind[F[_], R, A](command: F[R], cont: R => DoFree[F,A]) extends DoFree[F,A]
引入两种表达关系或者(计算的类型Return和Bind),前者表示计算结束,后者表示延续计算咯,所以我们的算法结构可以这么表示:
Bind(GetUp, (_:Unit) =>Bind(Wash, (_:Unit) =>Bind(MakeBreakfast, (breakfast: Breakfast) =>Bind(Eat(breakfast), (b: Breakfast) =>Bind(GoOut, (_:Unit) =>Return(())))))) : DoFree[GoToWork,Unit]
每一步计算都表示的很清楚,但是还不能用Monadic for-comprehension ,因为DoFree不是Monad啊,那么得...
trait DoFree[F[_],A] {def unit(a: A): DoFree[F,A] = Return(a)def flatMap[B](k: A => DoFree[F,B]): DoFree[F,B] = this match {case Return(a) =>k(a)case Bind(command, cont) =>Bind(command, cont andThen (_ flatMap k))}def map[B](f: A => B): DoFree[F,B] =flatMap(f andThen (Return(_)))
添加了flatMap方法,就可以用for了,flatMap的语义也很明确,把Retrun和Bind这两个结构串联起来。
这样我们就可以用for了:
object DoFree {def lift[F[_],R](command: F[R]): DoFree[F,R] =Bind(command, (x:R) => Return(x))}type FreeGoToWork[A] = DoFree[GoToWork, A]implicit def liftGoToWork[A](n: GoToWork[A]): FreeGoToWork[A] = DoFree.lift(n)val doo: FreeGoToWork[Unit] = for {_ <- GetUp_ <- Washbreakfast <- MakeBreakfast_ <- Eat(breakfast)_ <- GoOut} yield ()
好了,到此为止,我们的Free结束了,怎么可能呢?目前还没法执行啊!
trait ~>[F[_],G[_]] {def apply[A](fa: F[A]): G[A]}//在DoFree中添加方法:def foldMap[G[_]: Monad](f: F ~> G): G[A] = {val G = implicitly[Monad[G]]this match {case Return(a) =>G.unit(a)case Bind(command, cont) =>G.flatMap(f(command))(cont andThen (_ foldMap f))}}
"~>" 表示将F[A]转换为G[A]的函数,而G是个Monad
"foldMap"则是解开Return和Bind表示的计算结构,转化为真正monad G的for-comprehension
前方高能,这个"~>"可以将F[A]转为任意的Monad G[A],就是说DoFree表示的结构,可以翻译为任意的Monad表示的真正可计算的结构
下面来添加一种最简单的(G[A])标准实现 Id
sealed case class Id[A](a: A)//G必须提供Monadimplicit val IdMonad: Monad[Id] = new Monad[Id] {def unit[A](a: A) = Id(a)def flatMap[A,B](m: Id[A])(k: A => Id[B]): Id[B] =m match { case Id(a) => k(a) }}
3.5 实现一种"~>", 一般都已Console为例:
object ConsoleEffect extends (GoToWork ~> Id) {def apply[A](nl: GoToWork[A]): Id[A] = nl match {case GetUp =>println("get up")case Wash =>println("wash")case MakeBreakfast =>Breakfast(2, 1)case Eat(b) =>println(s"eat $b")case GoOut =>println(s"go out")}}
我们的Free完成,你可以随意实现"~>"和G[A], 最后就是执行了:
doo.foldMap(ConsoleEffect)//打印get upwasheat Breakfast(2,1)go out
故事到此结束就好了,这里的foldMap是个非尾递归函数,也就是说可能会stackoverflow,所以如果你去看Scalaz的Free会发现它表示计算的结构有三种:
private case class FlatMap[B](a: Free[F,A], f: A => Free[F,B]) extends Free[F,B]case class Return[F[_],A](a: A) extends Free[F,A]case class Suspend[F[_],A](ffa: F[Free[F,A]]) extends Free[F,A]
F[Free[F,A]]才是王道啊!这是啥?那是因为它引入了一个resume函数,来表示单步计算:
def resume(implicit F: Functor[F]): Either[F[Free[F,A]],A] = this match {case Return(a) => Right(a)case Suspend(k) => Left(k)case FlatMap(a,f) => a match {case Return(b) => f(b).resumecase Suspend(k) => Left(F.map(k)(_ flatMap f))case FlatMap(b,g) => FlatMap(b, g andThen (_ flatMap f)).resume}}
返回结构里有个F[Free[F,A]],引入resume的目的是为了TCO, 用堆内存替换栈内存,防止stackoverflow,resume对入参F[A]的要求是它必须是个Functor
详情可参考: [ http://days2012.scala-lang.org/sites/days2012/files/bjarnason_trampolines.pdf ]
但是不是所有东西都可以抽象出函子来的(反正臣妾做不到),比如我们上面的GoToWork[A]
这个时候范畴论又出来了,我们有Coyoneda啊!这是什么鬼? F[A] 可以变成 Coyoneda[F[], A],Coyoneda[F[], A]和 Yoneda[F[],A]互转, Coyoneda[F[],A]可转为F[A],但要提供Functor, Yoneda[F[],A]无需F: Functor可转为F[A], 反正可以转来转去,但是有一点很重要:F[A] 转 Coyoneda 不需要提供Functor,所以就可以抽象出一个FreeC并且是Stackless的去将任何F[A] 变成Free Monad, F[A] => Coyoneda[F[],A] => FreeC[S[_], A]
具体实现可参考: [ https://github.com/MiloXia/parz/blob/master/src/main/scala/com/mx/fp/core/stackless/Free.scala ]