@MiloXia
2015-07-20T18:20:54.000000Z
字数 4508
阅读 2004
函数式编程
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
_ <- Wash
breakfast <- 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必须提供Monad
implicit 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 up
wash
eat 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).resume
case 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 ]