我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码。像通过键盘显示器进行交流、读写文件、数据库等这些IO操作都会产生副作用。那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段烧CPU的代码,没有任何意义,所以任何类型的程序都必须具备IO功能,而在FP模式中对IO操作有特别的控制方式:具体实现是通过把代码中产生副作用的部分抽离出来延后运算(在所有纯代码运算之后)。scalaz的IO Monad就是处理副作用代码延后运算的一种数据结构。我先举个简单的例子来示范如何通过一种数据结构来实现对副作用代码的延迟运算:人机交互是一种典型的IO,有键盘输入,又有显示屏输出。println,readLine都会产生副作用,我们必须用一种数据类型来实现副作用代码抽离及延后运算,这种类型就是IO。我们先看看这个例子:我们希望实现人机交互如下:
def ask(prompt: String): String = { println(prompt) readLine } def tell(msg: String): Unit = println(msg) for { name <- ask("what's your name?") _ <- tell(s"I'm $name") } yield()
trait MyIO[+A] {self => def run: A def map[B](f: A => B): MyIO[B] = new MyIO[B] { def run = f(self.run) } def flatMap[B](f: A => MyIO[B]): MyIO[B] = new MyIO[B] { def run = f(self.run).run } } object MyIO { def apply[A](a: A) = new MyIO[A] { def run = a } implicit val ioMonad = new Monad[MyIO] { def point[A](a: => A) = new MyIO[A] { def run = a } def bind[A,B](ma: MyIO[A])(f: A => MyIO[B]): MyIO[B] = ma flatMap f } }
import MyIO._ def ask(prompt: String): MyIO[String] = MyIO { println(prompt) readLine } def tell(msg: String): MyIO[Unit] = MyIO { println(msg) }
val org: MyIO[Unit] = for { first <- ask("What's your first name?") last <- ask("What's your last name?") _ <- tell(s"Hello $first $last!") } yield()
object MyIOApp extends App { import MyIOFunctions._ pr.run } //运算结果: What's your first name? Tiger What's your last name? Chan Hello Tiger Chan!
package demo.app import scalaz._ import Scalaz._ trait MyIO[+A] {self => def run: A def map[B](f: A => B): MyIO[B] = new MyIO[B] { def run = f(self.run) } def flatMap[B](f: A => MyIO[B]): MyIO[B] = new MyIO[B] { def run = f(self.run).run } } object MyIO { def apply[A](a: A) = new MyIO[A] { def run = a } implicit val ioMonad = new Monad[MyIO] { def point[A](a: => A) = new MyIO[A] { def run = a } def bind[A,B](ma: MyIO[A])(f: A => MyIO[B]): MyIO[B] = ma flatMap f } } object MyIOFunctions { import MyIO._ def ask(prompt: String): MyIO[String] = MyIO { println(prompt) readLine } def tell(msg: String): MyIO[Unit] = MyIO { println(msg) } val prg: MyIO[Unit] = for { first <- ask("What's your first name?") last <- ask("What's your last name?") _ <- tell(s"Hello $first $last!") } yield() } object MyIOApp extends App { import MyIOFunctions._ prg.run }
sealed abstract class IO[A] { private[effect] def apply(rw: Tower[IvoryTower]): Trampoline[(Tower[IvoryTower], A)] ... /** Continues this action with the given function. */ def map[B](f: A => B): IO[B] = io(rw => apply(rw) map { case (nw, a) => (nw, f(a)) }) /** Continues this action with the given action. */ def flatMap[B](f: A => IO[B]): IO[B] = io(rw => apply(rw) flatMap { case (nw, a) => f(a)(nw) }) ... /** Construct an IO action from a world-transition function. */ def io[A](f: Tower[IvoryTower] => Trampoline[(Tower[IvoryTower], A)]): IO[A] = new IO[A] { private[effect] def apply(rw: Tower[IvoryTower]) = Free(() => f(rw)) }
object IO extends IOInstances { def apply[A](a: => A): IO[A] = io(rw => return_(rw -> a)) ...
/** Suspend the given computation in a single step. */ def return_[S[_], A](value: => A)(implicit S: Applicative[S]): Free[S, A] = liftF[S, A](S.point(value))
sealed abstract class IO[A] { private[effect] def apply(rw: Tower[IvoryTower]): Trampoline[(Tower[IvoryTower], A)] import IO._ /** * Runs I/O and performs side-effects. An unsafe operation. * Do not call until the end of the universe. */ def unsafePerformIO(): A = apply(ivoryTower).run._2
先用apply建Trampoline,再运行Free.run(Trampoline[A]=Free[Function0,A])。注意,我们并没有采用这个Tower[IvoryTower]。再者,函数unsafePerformIO是通过private函数apply先构建了Trampoline后再进行运算的。换言之IO Monad的用户是无法自定义算法(interpreter)的。我们前面曾经把Free描述成可以自定义F[A]编程语言的数据结构,那么IO[A]就是一种固定的FP编程语言,它只有unsafePerformIO一种算法(interpreter)。
IO Monad可以使我们更方便地在IO这个壳子里进行我们熟悉的行令编程(imperative programming),因为我们只需要把行令程序直接放进IO里就行了。看看下面这些例子:
val hello = print("hello ").point[IO] //> hello : scalaz.effect.IO[Unit] = scalaz.effect.IO$$anon$6@145eaa29 val world = IO (print("world,")) //> world : scalaz.effect.IO[Unit] = scalaz.effect.IO$$anon$6@57c758ac val howareyou = io {rw => return_(rw -> println("how are you!"))} //> howareyou : scalaz.effect.IO[Unit] = scalaz.effect.IO$$anon$6@a9cd3b1 val greeting = hello |+| world |+| howareyou //> greeting : scalaz.effect.IO[Unit] = scalaz.effect.IO$$anon$6@481a996b greeting.unsafePerformIO //> hello world,how are you!
import scalaz._ import Scalaz._ import effect._ import IO._ import Free._ import scala.language.higherKinds import scala.language.implicitConversions object IOPrg { def div(dvdn: Int, dvsor: Int): IO[Int] = IO(dvdn / dvsor) val ioprg: IO[Int] = for { _ <- putLn("enter dividend:") dvdn <- readLn _ <- putLn("enter divisor:") dvsor <- readLn quot <- div(dvdn.toInt, dvsor.toInt) _ <- putLn(s"the result:$quot") } yield quot } object IOMonadDemo extends App { import IOPrg._ ioprg.unsafePerformIO() }
"enter dividend:" 10 "enter divisor:" 5 "the result:2"
implicit def ioToOptionT[A](io: IO[A]): OptionT[IO,A] = io.liftM[OptionT] val optionIOprg: OptionT[IO,Int] = for { _ <- putLn("enter dividend:") dvdn <- readLn _ <- putLn("enter divisor:") dvsor <- readLn quot <- div(dvdn.toInt, dvsor.toInt) _ <- putLn(s"the result:$quot") } yield quit ... object IOMonadDemo extends App { import IOPrg._ // ioprg.unsafePerformIO() optionIOprg.run.unsafePerformIO() } ... "enter dividend:" 10 "enter divisor:" 5 "the result:2"
val optionIOprg: OptionT[IO,Int] = for { _ <- putLn("enter dividend:").liftM[OptionT] dvdn <- readLn.liftM[OptionT] _ <- putLn("enter divisor:").liftM[OptionT] dvsor <- readLn.liftM[OptionT] a <- if (dvsor.toInt == 0 ) OptionT(IO(None: Option[String])) else IO(0).liftM[OptionT] quot <- div(dvdn.toInt, dvsor.toInt).liftM[OptionT] _ <- putLn(s"the result:$quot").liftM[OptionT] } yield quit ... "enter dividend:" 10 "enter divisor:" 5 "the result:2" Process finished with exit code 0 ... "enter dividend:" 10 "enter divisor:" 0 Process finished with exit code 0
同样如果我们希望把用户的输入记录下来,我们可以用Writer的功能来实现。下一个例子就是Writer-IO Monad Transformer示范了:
type WriterTIO[F[_],A] = WriterT[F,List[String],A] val writerIOprg: WriterT[IO,List[String],Int] = for { _ <- putLn("enter dividend:").liftM[WriterTIO] dvdn <- readLn.liftM[WriterTIO] _ <- WriterT.writerT((List(s"received dividend $dvdn"),dvdn).point[IO]) _ <- putLn("enter divisor:").liftM[WriterTIO] dvsor <- readLn.liftM[WriterTIO] _ <- WriterT.writerT(IO(List(s"received divisor $dvsor, ready to divide ..."),dvdn)) quot <- div(dvdn.toInt, dvsor.toInt).liftM[WriterTIO] _ <- putLn(s"the result:$quot").liftM[WriterTIO] } yield quit ... object IOMonadDemo extends App { import IOMonadPrg._ // ioprg.unsafePerformIO() //optionIOprg.run.unsafePerformIO() println(writerIOprg.run.unsafePerformIO()) } ... "enter dividend:" 10 "enter divisor:" 5 "the result:2" (List(received dividend 10, received divisor 5, ready to divide ...),2) Process finished with exit code 0
用WriterT可以达到logging目的。当然,我们可以同时拥有Option和Writer的作用,这时的Monad Transformer就是三层的了,我们在前面的这篇讨论也做过示范。
最后看个异常处理示范:
type WriterTIO[F[_],A] = WriterT[F,List[String],A] val writerIOprg: WriterT[IO,List[String],Int] = for { _ <- putLn("enter dividend:").liftM[WriterTIO] dvdn <- readLn.liftM[WriterTIO] _ <- WriterT.writerT((List(s"received dividend $dvdn;"),dvdn).point[IO]) _ <- putLn("enter divisor:").liftM[WriterTIO] dvsor <- readLn.liftM[WriterTIO] _ <- WriterT.writerT(IO(List(s"received divisor $dvsor, ready to divide ..."),dvdn)) quot <- div(dvdn.toInt, dvsor.toInt).except(e => IO({println(e.getMessage());-99})).liftM[WriterTIO] _ <- if (quot < 0) WriterT.writerT((List(s"divide by zero Error!!!"),-99).point[IO]) else putLn(s"the result:$quot").liftM[WriterTIO] } yield (quot) ... object IOMonadDemo extends App { import IOMonadPrg._ // ioprg.unsafePerformIO() //optionIOprg.run.unsafePerformIO() println(writerIOprg.run.unsafePerformIO()) ... "enter dividend:" 3 "enter divisor:" 0 / by zero (List(received dividend 3;, received divisor 0, ready to divide ..., divide by zero Error!!!),-99) Process finished with exit code 0
/** Executes the handler if an exception is raised. */ def except(handler: Throwable => IO[A]): IO[A] = io(rw => try { Free.pure(this(rw).run) } catch { case e: Throwable => handler(e)(rw) }) /** * Executes the handler for exceptions that are raised and match the given predicate. * Other exceptions are rethrown. */ def catchSome[B](p: Throwable => Option[B], handler: B => IO[A]): IO[A] = except(e => p(e) match { case Some(z) => handler(z) case None => throw e }) /** * Returns a disjunction result which is right if no exception was raised, or left if an * exception was raised. */ def catchLeft: IO[Throwable \/ A] = map(\/.right[Throwable, A]) except (t => IO(-\/(t))) /**Like "catchLeft" but takes a predicate to select which exceptions are caught. */ def catchSomeLeft[B](p: Throwable => Option[B]): IO[B \/ A] = catchLeft map (_.leftMap(e => p(e).getOrElse(throw e))) /**Like "finally", but only performs the final action if there was an exception. */ def onException[B](action: IO[B]): IO[A] = this except (e => for { _ <- action a <- (throw e): IO[A] } yield a)
package demo.app import scalaz._ import Scalaz._ import effect._ import IO._ import Free._ import scala.language.higherKinds import scala.language.implicitConversions object IOMonadPrg { def div(dvdn: Int, dvsor: Int): IO[Int] = IO(dvdn / dvsor) val ioprg: IO[Int] = for { _ <- putLn("enter dividend:") dvdn <- readLn _ <- putLn("enter divisor:") dvsor <- readLn quot <- div(dvdn.toInt, dvsor.toInt) _ <- putLn(s"the result:$quot") } yield quot //implicit def ioToOptionT[A](io: IO[A]): OptionT[IO,A] = io.liftM[OptionT] val optionIOprg: OptionT[IO,Int] = for { _ <- putLn("enter dividend:").liftM[OptionT] dvdn <- readLn.liftM[OptionT] _ <- putLn("enter divisor:").liftM[OptionT] dvsor <- readLn.liftM[OptionT] a <- if (dvsor.toInt == 0 ) OptionT(IO(None: Option[String])) else IO(0).liftM[OptionT] quot <- div(dvdn.toInt, dvsor.toInt).liftM[OptionT] _ <- putLn(s"the result:$quot").liftM[OptionT] } yield quot type WriterTIO[F[_],A] = WriterT[F,List[String],A] val writerIOprg: WriterT[IO,List[String],Int] = for { _ <- putLn("enter dividend:").liftM[WriterTIO] dvdn <- readLn.liftM[WriterTIO] _ <- WriterT.writerT((List(s"received dividend $dvdn;"),dvdn).point[IO]) _ <- putLn("enter divisor:").liftM[WriterTIO] dvsor <- readLn.liftM[WriterTIO] _ <- WriterT.writerT(IO(List(s"received divisor $dvsor, ready to divide ..."),dvdn)) quot <- div(dvdn.toInt, dvsor.toInt).except(e => IO({println(e.getMessage());-99})).liftM[WriterTIO] _ <- if (quot < 0) WriterT.writerT((List(s"divide by zero Error!!!"),-99).point[IO]) else putLn(s"the result:$quot").liftM[WriterTIO] } yield (quot) } object IOMonadDemo extends App { import IOMonadPrg._ // ioprg.unsafePerformIO() //optionIOprg.run.unsafePerformIO() println(writerIOprg.run.unsafePerformIO()) }