泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(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)
很明显,declareWinner是个包含副作用的函数。它做了两件事:先对比两个Player的分数然后打印分数较大的Player。这里打印可以说是一项带有外作用的函数,我们试着把它分离出来:
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))
我们把分数比较代码winner分离了出来。我们还可以继续分解:printWinner也可以被认为做了两件事:先合成了一条信息,然后打印信息:
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))
这个例子看起来好像有些幼稚,但它示范了泛函编程的函数分解原理:我们并没有改变程序的功能,只是对程序代码进行了分解。把程序分解成更细的函数。实际上任何一个包含副作用的函数内都会有纯函数等待我们去分解。用一种代数关系表达就是:任何一个函数 A => B 都可以被分解成:
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))
现在函数printWinner已经变成了纯函数,它返回了一个IO值:这个IO值只是对一个副作用的描述但并没有运行它。只有这个IO类型的解译器(interpreter)在运算这个IO值时才会真正产生相应的副作用。
这里涉及到一些大的概念:编写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 {...}
}
既然IO类型是个Monad类型,那么我们就可以使用monadic语言编程了:
def ReadLine: IO[String] = IO { readLine }
def PrintLine(msg: String): IO[Unit] = IO { println(msg) }
def fahrenheitToCelsius(f: Double): Double =
(f -) * 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!!!")}
如上所示,任何副作用都可以被放入Delay。如果我们希望更好控制使用外界影响,可以把External的选项作为IO的类参数:
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]
我们只是把External换成了F,然后把它放进IO类型参数。现在我们可以通过定义不同的F类型来获取不同的副作用,例如:
trait Console[A]
case object ReadLine extends Console[Option[String]]
case class PrintLine(msg: String) extends Console[Unit]
现在通过IO[Console,A]我们获得了只能对键盘显示屏进行读写的副作用。当然,我们还可以定义文件、数据库、网络读写这些IO能力的F类型。所以我们通过定义F来规范使用副作用。注意,即使在Console类型我们也无法获知副作用是否的确产生,这部分是由F类型的Interpreter在运算IO程序时才确定的。这不又是Free Monad分开独立考虑(separation of concern)的Interpreter部分嘛。这是我们可以把这部分延后分开考虑。
我们先看看如何计算这个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))
}
}
}
可以看出这个run函数是个递归算法:先计算F值然后再递归调用run运算所产生的IO值。
我们现在可以创建一个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 }
我们可以把这个IO类型的运算方式再概括一点:只要F类型是个Monad,那么我们就可以运算IO值:
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]
有了F类型的Monad实例,函数runM现在能运算IO类型的值了。
在以上的讨论过程中我们得出了这样的结论:F类型代表了IO类型的Interpreter,我们不需要知道它到底产生副作用与否或者怎样产生。我们用F类型来把副作用的使用推延到F类型的实例。我们的IO程序只是对IO算法的描述。如何运算IO值包括如何使用副作用则在运算Interpreter时才体现。
作为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]
它们有许多相似点。最主要的是它们都是循环递归结构,能实现以heap换stack目的。我们可以把Trampoline类型的算法引进到IO类型中,这样就可以有效防止StackOverflow问题。实际上IO类型与Trampoline类型的深度抽象类型Free Monad更为相似:
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]
我们先用Free Monadic编程语言来描述IO算法:
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))
只要我们在Interpret程序里把GetLine,PutLine对应到InLog,OutLog两个List的读写。
如果我们需要采用无独占(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)
}
}
}
从以上的讨论我们得出:IO类型可以用Free类型代替,这样我们可以充分利用Free Monad的Monadic编程语言编写IO程序,我们又可以分开考虑编写可能产生IO副作用的Interpreter。
泛函编程(32)-泛函IO:IO Monad的更多相关文章
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(30)-泛函IO:Free Monad-Monad生产线
在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...
- 泛函编程(38)-泛函Stream IO:IO Process in action
在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...
- 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink
上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...
- 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process
IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(34)-泛函变量:处理状态转变-ST Monad
泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Re ...
- 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...
- 泛函编程(25)-泛函数据类型-Monad-Applicative
上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...
随机推荐
- 跨终端 Web
跨终端 Web(移动优先|响应式|HTML5|Hybrid|桌面+移动应用|一线前端负责人联袂推荐) 徐凯 著 ISBN 978-7-121-23345-6 2014年6月出版 定价:55.00 ...
- DataGridView的Cell事件的先后触发顺序
最近正在使用“DataGridView”对一个旧的Vs 2003开发的WINDOWS应用程序进行改造. 发现Vs 2003中的"DataGrid"中的一些事件已经在新的控件Data ...
- How Google TestsSoftware - A Break for Q&A
New material for the thisseries is coming more slowly. I am beginning to get into areas where I want ...
- Java并发包中CountDownLatch的工作原理、使用示例
1. CountDownLatch的介绍 CountDownLatch是一个同步工具,它主要用线程执行之间的协作.CountDownLatch 的作用和 Thread.join() 方法类似,让一些线 ...
- 【WP 8.1开发】如何处理摄像头翻转的问题
模拟器就像我们儿时的梦境,在其上运行应用程序时,一切总是那么美好的:而真机测试如同我们这个纷乱无章的现实世界,你会遇到各种小人和畜生,常常会遭受莫名的挫折.面对挫折,有人迎难而上,或不予理采,走自己的 ...
- 【WP 8.1开发】手机客户端应用接收推送通知
上一篇文章中,已经完成了用于发送通知的服务器端,接下来我们就用这个服务端来测试一下. 在开始测试之前,我们要做一个接收通知的WP应用. 1.启动VS Express for Windows,新建项目, ...
- JAVA实现Excel的读写--jxl
前段时间因为开发网站的需要,研究了一下java实现excel的读写,一般当我们做管理软件时,都需要打印报表,报表如何制作呢?相信一定难为过大家,本篇就为大家揭开它的神秘面纱,学习完半篇,你一定会对报表 ...
- [OpenCV] Samples 04: contours2
要先变为二值图像:cvThreshold 提取轮廓:cvFindContours #include "opencv2/imgproc/imgproc.hpp" #include & ...
- redis session共享中的序列化问题
今天在做session对象存入redis(set方法)时,碰到一个空指针异常,代码如下: public class CheckAccount extends HttpServlet { public ...
- Spark入门实战系列--3.Spark编程模型(上)--编程模型及SparkShell实战
[注]该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取 .Spark编程模型 1.1 术语定义 l应用程序(Application): 基于Spar ...