由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码(impure code)函数中的纯代码部分(pure code)抽离出来形成独立的另一个纯函数。我们通过代码抽离把不纯代码逐步抽离向外推并在程序里形成一个纯代码核心(pure core)。这样我们就可以顺利地在这个纯代码核心中实现函数组合。IO Monad就是泛函编程处理副作用代码的一种手段。我们先用个例子来示范副作用抽离:
case class Player(name: String, score: Int) def printWinner(p: Player): Unit = println(p.name + " is the winner!") def declareWinner(p1: Player, p2: Player): Unit = if (p1.score > p2.score ) printWinner(p1) else printWinner(p2)
def printWinner(p: Player): Unit = println(p.name + " is the winner!") def winner(p1: Player, p2: Player): Player = if (p1.score > p2.score) p1 else p2 def declareWinner(p1: Player, p2: Player): Unit = printWinner(winner(p1, p2))
def winnerMsg(p: Player): String = p.name + " is the winner!" def printWinner(p: Player): Unit = println(winnerMsg(p)) def winner(p1: Player, p2: Player): Player = if (p1.score > p2.score) p1 else p2 def declareWinner(p1: Player, p2: Player): Unit = printWinner(winner(p1, p2))
1、一个纯函数:A => D, 这里D只是一个功能描述表达式
2、一个带副作用的非纯函数: D => B, 它可以被视为D的解译器(interpreter),把描述解译成有副作用的指令
在泛函编程中我们会持续进行这种函数分解(factoring),把含有副作用的代码分解提取出来向外推形成一个副作用代码层。这个非纯函数形成的代码层内部则是经分解形成的纯代码核心。最后我们到达了那些表面上已经无可分解的非纯函数如:println,它的类型是String => Unit, 接下去我们应该怎么办呢?
实际上通过增加一个新的数据类型IO我们甚至可以对println进行分解:
trait IO {def run: Unit } def printLine(line: String) : IO = new IO { def run = println(line) } def printWinner(p: Player): IO = printLine(winnerMsg(p)) case class Player(name: String, score: Int) def winnerMsg(p: Player): String = p.name + " is the winner!" def winner(p1: Player, p2: Player): Player = if (p1.score > p2.score) p1 else p2 def declareWinner(p1: Player, p2: Player): Unit = printWinner(winner(p1, p2))
这里涉及到一些大的概念:编写IO程序和运算IO值是相互分离的过程(separation of concern)。我们的IO程序用一系列表达式描述了要求的IO功能,而IO interpreter实现这些功能的方式可以是多样的,包括:外设读写,文件、数据库读写及并行读取等等。如何实现这些IO功能与IO程序编写无任何关系。
现在,有了这个IO类型,我们可以放心地用函数组合的泛函编程方式围绕着这个IO类型来编写IO程序,因为我们知道通过这个IO类型我们把副作用的产生推延到IO程序之外的IO解译器里,而IO编程与解译器是两个各自独立的程序。
泛函模式的IO编程就是把IO功能表达和IO副作用产生分开设计:IO功能描述使用基于IO Monad的Monadic编程语言,充分利用函数组合进行。而产生副作用的IO实现则推延到独立的Interpreter部分。当然,Interpreter也有可能继续分解,把产生副作用代码再抽离向外推延,这样我们还可以对Interpreter的纯代码核心进行函数组合。
我们上面的简版IO类型只代表输出类型(output type)。Input类型需要一个存放输入值的变量。在泛函编程模式里变量是用类型参数代表的:
trait IO[+A] { self => def run: A def map[B](f: A => B): IO[B] = new IO[B] { def run = f(self.run)} def flatMap[B](f: A => IO[B]): IO[B] = new IO[B] {def run = f(self.run).run} }
我们用run来对IO值进行计算。在上面我们已经实现了map和flatMap函数,所以这个IO类型就是个Monad。看下面:
object IO extends Monad[IO] { def unit[A](a: A) = new IO[A] {def run = a} def flatMap[A,B](ma: IO[A])(f: A => IO[B]) = ma flatMap f def map[A,B](ma: IO[A])(f: A => B) = ma map f def apply[A](a: A) = unit(a) //IO构建器,可以实现 IO {...} }
def ReadLine: IO[String] = IO { readLine } def PrintLine(msg: String): IO[Unit] = IO { println(msg) } def fahrenheitToCelsius(f: Double): Double = (f -32) * 5.0 / 9.0 def converter: IO[Unit] = for { _ <- PrintLine("Enter a temperature in degrees fahrenheit:") d <- ReadLine.map(_.toDouble) _ <- PrintLine(fahrenheitToCelsius(d).toString) } yield ()
我们再来看看这个IO类型:IO[A] { def run: A },从类型款式来看我们只知道IO[A]类型值是个延后值,因为A值是通过调用函数run取得的。实际情况是run在运算A值时run函数里的纯代码向程序外的环境提请一些运算要求如输入(readLine),然后把结果传递到另外一些纯代码;然后这些纯代码有可能又向外提请。我们根本无法确定副作用是在那个环节产生的。如此可以确定,这个IO类型无法完整地表达IO运算。我们必需对IO类型进行重新定义:
trait IO[A] {def run: A} case class Pure[+A](a: A) extends IO[A] case class Request[Extenal[_],I,A](expr: Extenal[I], cont: I => IO[A]) extends IO[A]
这个IO类型把纯代码与副作用代码分开两种IO运算状态:IO运算可以是一个纯函数值,或者是一个外部副作用运算请求。这个External类型定义了外部副作用运算方式,它决定了我们程序能获得什么样的外部副作用运算。这个External[I]就像一个表达式,但只能用外部运算IO的程序来运算它。cont函数是个接续函数,它决定了获取External[I]运算结果后接着该做些什么。
现在我们可以明确分辨一个运算中的纯函数和副作用函数。但是我们还无法控制External类型的行为。External[I]可以代表一个简单的推延值,如下:
trait Runnable[A] { def run: A } object Delay { def apply[A](a: A) = new Runnable[A] { def run = a} } Delay {println("SIDE EFFECTS!!!")}
trait IO[F[_],+A] {} case class Pure[F[_],+A](get: A) extends IO[F,A] case class Request[F[_],I,+A](expr: F[I], cont: I => IO[F,A]) extends IO[F,A]
trait Console[A] case object ReadLine extends Console[Option[String]] case class PrintLine(msg: String) extends Console[Unit]
我们先看看如何计算这个IO类型的值:
trait Run[F[_]] { def apply[A](expr: F[A]): (A, Run[F]) } object IO { def run[F[_],A](R: Run[F])(io: IO[F,A]): A = io match { case Pure(a) => a case Request(expr, cont) => R(expr) match { case (a,r2) => run(r2)(cont(a)) } } }
我们现在可以创建一个F类型的实例然后运算IO:
trait Console[A] case object ReadLine extends Console[Option[String]] case class PrintLine(msg: String) extends Console[Unit] object RunConsole extends Run[Console] { def apply[A](c: Console[A]): (A, Run[Console]) = c match { case ReadLine => { val r = try Some(readLine) catch { case _ => None } (r, RunConsole) } case PrintLine(m) => (println(m),RunConsole) } } IO.run(RunConsole)(ioprg)
实际上这个IO类型是个Monad,因为我们可以实现它的unit和flatMap函数:
trait IO[F[_],A] { def unit(a: A) = Pure(a) def flatMap[B](f: A => IO[F,B]): IO[F,B] = this match { case Pure(a) => f(a) // case Request(expr,cont) => Request(expr, cont andThen (_ flatMap f)) case Request(expr,cont) => Request(expr, (x: Any) => cont(x) flatMap f) } def map[B](f: A => B): IO[F,B] = flatMap(a => Pure(f(a))) } case class Pure[F[_],A](get: A) extends IO[F,A] case class Request[F[_],I,A](expr: F[I], cont: I => IO[F,A]) extends IO[F,A]
它的Monad实例如下:
def ioMonad[F[_]] = new Monad[({type l[x] = IO[F, x]})#l] { def unit[A](a: A) = Pure(a) def flatMap[A,B](fa:IO[F,A])(f: A => IO[F,B]): IO[F,B] = fa flatMap f def map[A,B](fa: IO[F,A])(f: A => B): IO[F,B] = fa map f }
trait IO[F[_],A] { def unit(a: A) = Pure(a) def flatMap[B](f: A => IO[F,B]): IO[F,B] = this match { case Pure(a) => f(a) // case Request(expr,cont) => Request(expr, cont andThen (_ flatMap f)) case Request(expr,cont) => Request(expr, (x: Any) => cont(x) flatMap f) } def map[B](f: A => B): IO[F,B] = flatMap(a => Pure(f(a))) def runM[F[_],A](F: Monad[F])(io: IO[F,A]): F[A] = io match { case Pure(a) => F.unit(a) // case Request(expr, cont) => F.flatMap(expr)(cont andThen (_.runM(F)(io))) case Request(expr, cont) => F.flatMap(expr)(x => cont(x).runM(F)(io)) } } case class Pure[F[_],A](get: A) extends IO[F,A] case class Request[F[_],I,A](expr: F[I], cont: I => IO[F,A]) extends IO[F,A]
作为IO算法,首先必须注意防止的就是递归算法产生的堆栈溢出问题。运算IO值的runM是个递归算法,那我们必须保证至少它是一个尾递归算法。当然,我们前面讨论的Trampoline类型是最佳选择。我们可以比较一下IO和Trampoline类型结构:
trait IO[F[_],A] { def unit(a: A) = Pure(a) def flatMap[B](f: A => IO[F,B]): IO[F,B] = this match { case Pure(a) => f(a) // case Request(expr,cont) => Request(expr, cont andThen (_ flatMap f)) case Request(expr,cont) => Request(expr, (x: Any) => cont(x) flatMap f) } def map[B](f: A => B): IO[F,B] = flatMap(a => Pure(f(a))) } case class Pure[F[_],A](get: A) extends IO[F,A] case class Request[F[_],I,A](expr: F[I], cont: I => IO[F,A]) extends IO[F,A] trait Trampoline[A] { def unit(a: A): Trampoline[A] = Done(a) def flatMap[B](f: A => Trampoline[B]): Trampoline[B] = this match { case Done(a) => f(a) case More(k) => k() flatMap f } def map[B](f: A => B): Trampoline[B] = flatMap(a => Done(f(a))) } case class Done[A](a: A) extends Trampoline[A] case class More[A](k: () => Trampoline[A]) extends Trampoline[A]
trait Free[F[_],A] { private case class FlatMap[B](a: Free[F,A], f: A => Free[F,B]) extends Free[F,B] def unit(a: A): Free[F,A] = Return(a) def flatMap[B](f: A => Free[F,B])(implicit F: Functor[F]): Free[F,B] = this match { case Return(a) => f(a) case Suspend(k) => Suspend(F.map(k)(a => a flatMap f)) case FlatMap(b,g) => FlatMap(b, g andThen (_ flatMap f)) } def map[B](f: A => B)(implicit F: Functor[F]): Free[F,B] = flatMap(a => Return(f(a))) } 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]
Free类型的FlatMap结构和IO类型的Request结构极其相像。我们在前面的讨论中已经介绍了Free类型:首先它是一个为支持尾递归算法而设计的结构,是由一个Functor F产生的Monad。Free的功能由Monad和Interpreter两部分组成:Monad部分使我们可以使用Monadic编程语言来描述一些算法,Interpreter就是F类型,必须是个Functor,它负责描述副作用行为。只有在运算算法时才真正产生副作用。我们可以直接使用Free类型代表IO运算:用Free的Monadic编程语言来描述IO算法,用Interpreter来描述IO效果,用Free的Trampoline运算机制实现尾递归运算。现在我们先看看完整的Free类型:
trait Free[F[_],A] { private case class FlatMap[B](a: Free[F,A], f: A => Free[F,B]) extends Free[F,B] def unit(a: A): Free[F,A] = Return(a) def flatMap[B](f: A => Free[F,B])(implicit F: Functor[F]): Free[F,B] = this match { case Return(a) => f(a) case Suspend(k) => Suspend(F.map(k)(a => a flatMap f)) case FlatMap(b,g) => FlatMap(b, g andThen (_ flatMap f)) } def map[B](f: A => B)(implicit F: Functor[F]): Free[F,B] = flatMap(a => Return(f(a))) 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 } } def liftF(fa: F[A])(implicit F: Functor[F]): Free[F,A] = Suspend(F.map(fa)(Return(_))) } 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]
trait Console[A] case class GetLine[A](next: A) extends Console[A] case class PutLine[A](msg: String, next: A) extends Console[A] implicit val consoleFunctor = new Functor[Console]{ def map[A,B](ca: Console[A])(f: A => B): Console[B] = ca match { case GetLine(a) => GetLine(f(a)) case PutLine(m,a) => PutLine(m,f(a)) } } //> consoleFunctor : ch13.ex3.Functor[ch13.ex3.Console] = ch13.ex3$$anonfun$ma //| in$1$$anon$1@53e25b76 type ConsoleIO[A] = Free[Console,A] implicit def liftConsole[A](ca: Console[A]) = Free.liftF(ca) //> liftConsole: [A](ca: ch13.ex3.Console[A])ch13.ex3.Free[ch13.ex3.Console,A] def putLine(msg: String) = PutLine(msg,()) //> putLine: (msg: String)ch13.ex3.PutLine[Unit] def getLine = GetLine(()) //> getLine: => ch13.ex3.GetLine[Unit] val ioprg:ConsoleIO[Unit] = for { _ <- putLine("What is your first name ?") first <- getLine _ <- putLine("What is your last name ?") last <- getLine _ <- putLine(s"Hello, $first $last !") } yield() //> ioprg : ch13.ex3.Free[ch13.ex3.Console,Unit] = Suspend(PutLine(What is you //| r first name ?,Suspend(GetLine(Suspend(PutLine(What is your last name ?,Sus //| pend(GetLine(Suspend(PutLine(Hello, () () !,Return(())))))))))))
现在我们用Monadic编程语言描述了一个IO程序,下一步就是运算这个IO程序从而获得它的值。如何运算IO值是Interpreter的功能。这个过程可能会产生副作用。至于如何产生副作用,产生什么样的副作用则由Interpreter程序描述。IO值运算过程就是一个由Monadic IO功能描述到IO影响产生方式Interpret语句的语言转换(interpret,翻译)。我们可以来看看这个运算函数:
trait ~>[F[_],G[_]]{ def apply[A](fa: F[A]): G[A] } trait Free[F[_],A] { private case class FlatMap[B](a: Free[F,A], f: A => Free[F,B]) extends Free[F,B] def unit(a: A): Free[F,A] = Return(a) def flatMap[B](f: A => Free[F,B])(implicit F: Functor[F]): Free[F,B] = this match { case Return(a) => f(a) case Suspend(k) => Suspend(F.map(k)(a => a flatMap f)) case FlatMap(b,g) => FlatMap(b, g andThen (_ flatMap f)) } def map[B](f: A => B)(implicit F: Functor[F]): Free[F,B] = flatMap(a => Return(f(a))) 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 } } def foldMap[G[_]](f: (F ~> G))(implicit F: Functor[F], G: Monad[G]): G[A] = resume match { case Right(a) => G.unit(a) case Left(k) => G.flatMap(f(k))(_ foldMap f) } } 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] object Free { def liftF[F[_],A](fa: F[A])(implicit F: Functor[F]): Free[F,A] = Suspend(F.map(fa)(Return(_))) }
这个foldMap就是一个IO程序运算函数。由于它是一个循环计算,所以通过resume函数引入Trampoline尾递归计算方式来保证避免StackOverflow问题发生。foldMap函数将IO描述语言F解译成可能产生副作用的G语言。在解译的过程中逐步用flatMap运行非纯代码。
我们可以用Free Monad的结构替代IO类型结构,这样我们就可以用Monadic编程语言来描述IO程序。至于实际的IO副作用如何,我们只知道产生副作用的Interpret程序是个Monad,其它一无所知。
现在我们可以进入Interpreter编程了:
type Id[A] = A implicit val idMonad = new Monad[Id] { def unit[A](a: A): A = a def flatMap[A,B](fa: A)(f: A => B): B = f(fa) } object ConsoleEffect extends (Console ~> Id) { def apply[A](c: Console[A]): A = c match { case GetLine(n) => readLine ; n case PutLine(m,n) => println(m); n } } ioprg.foldMap(ConsoleEffect)
我们说过:运算IO值就是把IO程序语言逐句翻译成产生副作用的Interpreter语言这个过程。在以上例子里我们采用了Id Monad作为Interpreter语言。Id Monad的flatMap不做任何事情,所以IO程序被直接对应到基本IO函数readLine, println上了。
我们也可以把副作用变成对List进行读写:
case class InOutLog(inLog: List[String], outLog: List[String]) case class Logger[A](runLogger: InOutLog => (A, InOutLog)) object MockConsole extends (Console ~> Logger) { def apply[A](c: Console[A]): Logger[A] = Logger[A]( s => (c, s) match { case (GetLine(n), InOutLog(in,out)) => (in.head, InOutLog(in.tail,out)) case (PutLine(l,n), InOutLog(in,out)) => ((), InOutLog(in, l :: out)) } ) } val s = ioprg.foldMap(MockConsole) s.runLogger(InOutLog(List("Tiger","Chan"),Nil))
如果我们需要采用无独占(Non-blocking)读写副作用的话可以这样改写Interpreter:
object NonBlockingIO extends(Console ~> Future) { def apply[A](c: Console[A]): Future[A] = c match { case GetLine(n) => Future.unit { try Some(readLine) catch {case _: Exception => None} } case PutLine(n,l) => Future.unit{ println(l) } } }