我们不断地重申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()

ask和tell分别返回String和Unit,它们都是副作用即时产生的结果。ask和tell都是非纯函数。我们可以设计一个类型来实现副作用代码抽离:

 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
}
}

现在我们可以把ask和tell函数的返回类型改成MyIO:

 import MyIO._
def ask(prompt: String): MyIO[String] =
MyIO {
println(prompt)
readLine
}
def tell(msg: String): MyIO[Unit] =
MyIO {
println(msg)
}

MyIO是个Monad,我们可以在for-comprehension里用行令方式编程了:

 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()

注意,在这个阶段我们只完成了对一个程序功能的描述,实际运算是在MyIO.run那里:

 object MyIOApp extends App {
import MyIOFunctions._
pr.run
}
//运算结果:
What's your first name?
Tiger
What's your last name?
Chan
Hello Tiger Chan!

run是MyIO类型的interpreter。现在我们已经实现了程序描述(算式)和运算(算法)的关注分离。而且我们可以随便使用ask和tell而不进行运算,延迟至调用run对MyIO类型进行运算。以上只是一种简单的示范IO类型,我先把完整的源代码提供如下:

 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
}

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 {
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))
}

可以看得出io[A](...)就是IO的构建器(constructor)。IO[A]类型是Free[Function0,A] = Free(() => f(rw)),最终就是一个Trampoline,这个可以从下面的apply看得出来:

object IO extends IOInstances {
def apply[A](a: => A): IO[A] =
io(rw => return_(rw -> a))
...

上面的Tower[IvoryTower]是状态切换函数的输入类型,不参与实际运算(不在任何运算中调用如: rw -> a),起一种状态标签作用(state tag)。主要提供给编译器(compiler)做类型安全用。实际上这个状态切换函数就是一个延迟运算值 => A。io把这个值挂到Free的suspend状态:

/** 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))

再看看IO的运算方式:

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!

这个例子示范了用三种方式把副作用语句print升格成IO。不要被IO[A]的IO字面误导了,IO[A]的这个A不一定是副作用命令,任何行令编程使用的语句都可以放人IO[_],包括变量申明、赋值、文件读写等。所以我们说IO Monad就是在FP模式中进行行令编程的通用方式。可以想象我们可能会在IO这个壳子内进行我们熟悉的程序编写。那么IO Monad到底能不能符合在FP环境内的行令编程要求呢?我们可以用几个例子来对流程控制(flow control),跟踪记录(logging)即异常处理(exception handling)等方面进行测试示范:

 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:"

"enter divisor:"

"the result:2"

ioprg是一段包含了带副作用语句的程序。所有副作用延迟到unsafePerformIO()才正真产生。现在我们先示范流程控制,试着提早跳出这个for-loop。这不就是option在for-comprehension的作用吗。我们需要在IO[A]这种Monad之上增加Option的作用,可以用Monad Transformer来实现:

   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:" "enter divisor:" "the result:2"

我们把结果类型变成Option[IO,Int]后并没有改变程序的功能。现在试试中途终止:

   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 == ) OptionT(IO(None: Option[String])) else IO().liftM[OptionT]
quot <- div(dvdn.toInt, dvsor.toInt).liftM[OptionT]
_ <- putLn(s"the result:$quot").liftM[OptionT]
} yield quit
...
"enter dividend:" "enter divisor:" "the result:2" Process finished with exit code
...
"enter dividend:" "enter divisor:" Process finished with exit code

不错,的确在录入0时可以中途退出。不过现在for-comprehension里的每句都需要用liftM[OptionT]来升格。无法像前面统统用implicit def ioToOptionT来自动升格,这是因为把None升格时会产生类型推导问题,这个就放在以后再研究了。

同样如果我们希望把用户的输入记录下来,我们可以用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:" "enter divisor:" "the result:2"
(List(received dividend , received divisor , ready to divide ...),) Process finished with exit code

用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());-})).liftM[WriterTIO]
_ <- if (quot < ) WriterT.writerT((List(s"divide by zero Error!!!"),-).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:" "enter divisor:" / by zero
(List(received dividend ;, received divisor , ready to divide ..., divide by zero Error!!!),-) Process finished with exit code

以上例子调用了scalaz IO Monad typeclass 提供的except方法,scalaz还提供了其它的异常处理函数:

 /** 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 == ) OptionT(IO(None: Option[String])) else IO().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());-})).liftM[WriterTIO]
_ <- if (quot < ) WriterT.writerT((List(s"divide by zero Error!!!"),-).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())
}

Scalaz(41)- Free :IO Monad-Free特定版本的FP语法的更多相关文章

  1. 泛函编程(32)-泛函IO:IO Monad

    由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...

  2. 关于在官网上查看和下载特定版本的webrtc代码

    注:这个方法已经不适用了,帖子没删只是留个纪念而已 gclient:如果不知道gclient是什么东西 ... 就别再往下看了. 下载特定版本的代码: #gclient sync --revision ...

  3. 更新xcode后插件失效问题——不针对特定版本的通用解决方法

    一.Xcode更新后插件失效的原理 1.每次更新Xcode后插件都会失效,其实插件都还在这个目录好好的躺着呢: ~/Library/Application Support/Developer/Shar ...

  4. 安装特定版本 sysbench 以及安装时指定特定的mysql客户端

    step1: 下载安装源码 https://github.com/akopytov/sysbench (git clone https://github.com/akopytov/sysbench.g ...

  5. Ubuntu安装特定版本安装包

    Ubuntu安装特定版本安装包可以用aptitude,aptitude是apt-get的高级版,使用起来更强大. aptitude install package=version 比如我要安装2.6. ...

  6. conda查找安装包的版本以及安装特定版本的包

    如下图 想要安装特定版本的torchvision,然后conda search torchvision,能够列出conda云上所有的安装包 然后,安装包的时候,conda install 包名=版本就 ...

  7. 【Ubuntu】Ubuntu中下载特定版本内核和设置某版本内核为默认启动内核

    0. 基本命令 uname -a # 查看当前所使用内核 dpkg -l | grep linux # dpkg后是lmn的l.查看当前操作系统的内核 dekg -l | grep linux-ima ...

  8. 七周七语言之用Io编写领域特定语言

    如果你想获得更好的阅读体验,可以前往我在 github 上的博客进行阅读,http://lcomplete.github.io/blog/2013/06/05/sevenlang-io/. Io 语言 ...

  9. .net , java webSocket 连接 Socket.io (1.4.4版本) 问题

    .net版Socketio4net类库和java版socket.io-java-client类库 连接socket.io 1.4版本都不行,网上大多是socket.io 0.9版本的,socket.i ...

随机推荐

  1. [常见问题]在Linux下执行Redis命令不起作用.

    redis 127.0.0.1:6379> 这个后面无论输入什么命令都没有返回 ok 或者其他的信息,一直保持截图的状态: 解决方法:在SecureCRT中设置Options-->Sess ...

  2. [Spring框架]Spring 事务管理基础入门总结.

    前言:在之前的博客中已经说过了数据库的事务, 不过那里面更多的是说明事务的一些锁机制, 今天来说一下Spring管理事务的一些基础知识. 之前的文章: [数据库事务与锁]详解一: 彻底理解数据库事务一 ...

  3. fir.im Weekly - 当技术成为一种 “武器”

    最近纷纷扰扰,快播公开庭审,携程事件仍在升级,百度还在继续无底线.我们相信技术本身并不可耻,但是用技术作恶就是可耻.当技术成为一种武器,Do not be evil. 好了,继续本期的 fir.im ...

  4. Android Activity 启动模式和任务栈

    在了解了基本的Activity的生命周期后,我们能够很好的在一个Activity上面做相关的业务.但是这是不够的,因为Android通过任务栈来保存整个APP的Activity,合理的调度任务栈才能够 ...

  5. python的继承

    继承是面向对象的重要特征之一,继承是两个类或者多个类之间的父子关系,子进程继承了父进程的所有公有实例变量和方法.继承实现了代码的重用.重用已经存在的数据和行为,减少代码的重新编写,python在类名后 ...

  6. DataGrid--多记录CRUD

    最近在做一个datagrid,但因为引用的Jquery,加上初学者,所以难免费尽周折.现在将完整版贴出来,跟大家分享,一起切磋,也方便自己回顾学习. ps:第一次发帖,不知排版效果如何,瑕疵勿怪. 首 ...

  7. 如何查看某个查询用了多少TempDB空间

        最近帮助客户调优的过程中,发现客户的TempDB存在非常大的压力,经过排查是发现某些语句对TempDB的巨量使用所导致.     在SQL Server中,TempDB主要负责供下述三类情况使 ...

  8. 【博客美化】04.自定义地址栏logo

    博客园美化相关文章目录: [博客美化]01.推荐和反对炫酷样式 [博客美化]02.公告栏显示个性化时间 [博客美化]03.分享按钮 [博客美化]04.自定义地址栏logo [博客美化]05.添加Git ...

  9. 反质数问题,求不大于n的最大反质数

    反质数:设f(n)表示n个约数的个数,如果对于任意x有0<x<n, f(x) < f(n),那么n就是一个反质数 我们都知道对于任意一个数n,都可以用质数乘积的形式表示出来:x = ...

  10. S Gallery – 很有特色的响应式 jQuery 相册插件

    S Gallery 是一款响应式的 jQuery 相册插件.使用了 HTML5 全屏 API 以及 CSS3 动画 和 CSS3 转换,所以只能在支持这些功能的浏览器中使用. 这款插件它有一个特色功能 ...