我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码。像通过键盘显示器进行交流、读写文件、数据库等这些IO操作都会产生副作用。那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段烧CPU的代码,没有任何意义,所以任何类型的程序都必须具备IO功能,而在FP模式中对IO操作有特别的控制方式:具体实现是通过把代码中产生副作用的部分抽离出来延后运算(在所有纯代码运算之后)。scalaz的IO Monad就是处理副作用代码延后运算的一种数据结构。我先举个简单的例子来示范如何通过一种数据结构来实现对副作用代码的延迟运算:人机交互是一种典型的IO,有键盘输入,又有显示屏输出。println,readLine都会产生副作用,我们?必须用一种数据类型来实现副作用代码抽离及延后运算,这种类型就是IO。我们先看看这个例子:我们希望实现人机交互如下:
1 def ask(prompt: String): String = { 2 println(prompt) 3 readLine 4 } 5 def tell(msg: String): Unit = println(msg) 6 for { 7 name <- ask("what's your name?") 8 _ <- tell(s"I'm $name") 9 } yield()
ask和tell分别返回String和Unit,它们都是副作用即时产生的结果。ask和tell都是非纯函数。我们可以设计一个类型来实现副作用代码抽离:
1 trait MyIO[+A] {self =>
2 def run: A 3 def map[B](f: A => B): MyIO[B] =
4 new MyIO[B] { 5 def run = f(self.run) 6 } 7 def flatMap[B](f: A => MyIO[B]): MyIO[B] =
8 new MyIO[B] { 9 def run = f(self.run).run 10 } 11 } 12 object MyIO { 13 def apply[A](a: A) = new MyIO[A] { def run = a } 14 implicit val ioMonad = new Monad[MyIO] { 15 def point[A](a: => A) = new MyIO[A] { def run = a } 16 def bind[A,B](ma: MyIO[A])(f: A => MyIO[B]): MyIO[B] =
17 ma flatMap f 18 } 19 }
现在我们可以把ask和tell函数的返回类型改成MyIO:
1 import MyIO._ 2 def ask(prompt: String): MyIO[String] =
3 MyIO { 4 println(prompt) 5 readLine 6 } 7 def tell(msg: String): MyIO[Unit] =
8 MyIO { 9 println(msg) 10 }
MyIO是个Monad,我们可以在for-comprehension里用行令方式编程了:
1 val org: MyIO[Unit] = for { 2 first <- ask("What's your first name?") 3 last <- ask("What's your last name?") 4 _ <- tell(s"Hello $first $last!") 5 } yield()
注意,在这个阶段我们只完成了对一个程序功能的描述,实际运算是在MyIO.run那里:
1 object MyIOApp extends App { 2 import MyIOFunctions._ 3 pr.run 4 } 5 //运算结果:
6 What's your first name?
7 Tiger 8 What's your last name?
9 Chan 10 Hello Tiger Chan!
run是MyIO类型的interpreter。现在我们已经实现了程序描述(算式)和运算(算法)的关注分离。而且我们可以随便使用ask和tell而不进行运算,延迟至调用run对MyIO类型进行运算。以上只是一种简单的示范IO类型,我先把完整的源代码提供如下:
1 package demo.app 2 import scalaz._ 3 import Scalaz._ 4
5 trait MyIO[+A] {self =>
6 def run: A 7 def map[B](f: A => B): MyIO[B] =
8 new MyIO[B] { 9 def run = f(self.run) 10 } 11 def flatMap[B](f: A => MyIO[B]): MyIO[B] =
12 new MyIO[B] { 13 def run = f(self.run).run 14 } 15 } 16 object MyIO { 17 def apply[A](a: A) = new MyIO[A] { def run = a } 18 implicit val ioMonad = new Monad[MyIO] { 19 def point[A](a: => A) = new MyIO[A] { def run = a } 20 def bind[A,B](ma: MyIO[A])(f: A => MyIO[B]): MyIO[B] =
21 ma flatMap f 22 } 23 } 24 object MyIOFunctions { 25 import MyIO._ 26 def ask(prompt: String): MyIO[String] =
27 MyIO { 28 println(prompt) 29 readLine 30 } 31 def tell(msg: String): MyIO[Unit] =
32 MyIO { 33 println(msg) 34 } 35 val prg: MyIO[Unit] = for { 36 first <- ask("What's your first name?") 37 last <- ask("What's your last name?") 38 _ <- tell(s"Hello $first $last!") 39 } yield() 40
41
42 } 43 object MyIOApp extends App { 44 import MyIOFunctions._ 45 prg.run 46 }
scalaz的IO Monad当然复杂的多。我们看看scalaz的IO Monad是怎样的:effect/IO.scala
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 { c