泛函编程(9)-异常处理-Option
Option是一种新的数据类型。形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里;然后在管子内部对数据进行各种操作。所以Option的数据操作与List很相似。不同的是Option的管子内最多只能存放一个元素,在这个方面Option的数据操作就比List简单的多,因为使用者不必理会数据元素的位置、顺序。Option只有两种状态:包含一个任何类型的元素或者为空。或者这样讲:一个Option实例包含 0 或 1 个元素;None代表为空,Some(x)代表包含一个任意类型的元素x。和List的两种状态:Nil及Cons很是相似。值得注意的是,这个为空的概念与java的null值有根本的区别:None或Nil值都具有明确的类型而null则可能是任何类型的数据。在java编程里我们通常需要单独附加一些程序来检查、处理null值,而None或Nil代表了一个类型数据的状态,可以直接使用。
既然Option与List高度相似,让我们把List的数据类型设计搬过来试试:
trait Option[+A]
case object None extends Option[Nothing]
case class Some[+A](value: A) extends Option[A]
这简直跟List一模样嘛。当然,结构是一样的,但因为Option最多可以有一个元素,所有的操作函数将会简洁的多。
那么为什么要增加一种数据类型?Option又是用来干什么的呢?
我们先拿个超简单的java例子来示范:
java code
double divide(double x, double y)
{
double ratio;
try {
z = x / y;
} catch (Exception e) {
//bala bala ,,,
return ?????
}
return ratio;
}
在写这段java程序时一个疑问立即跳了出来:如果出现了错误时这个函数该返回什么呢?函数申明divide返回double,但在发生运算错误后我们不能返回任何double值,任何double值都不正确。唯一选择就是通过异常处理(Exception Handling)来解决了。那是不是意味着这个函数的所有用户都必须自己增加一段代码去处理异常了呢?那么每个用户都必须这么写:
java code
try {
r = divide(x,y);
//bala bala ...
} catch (Exception e) {
//bala bala ,,,
// bala bala ...
}
这样做勉强可以继续编程,但最终程序变的一塌糊涂,增加了许多无谓的代码,也臃肿了整改程序,增加了编程人员阅读理解的难度。泛函编程的这个Option数据类型正是为解决这样的问题而增加的。如果以上问题用Scala来编写的话:
def divide(x: Double, y: Double): Option[Double] = {
try {
Some(x/y)
} catch {
case e: Exception => None
}
}
首先,不用再头疼该返回什么值了:出问题就直接返回None。不过使用者必须从Option这个管子里先把值取出来,看起来好像又多了一道手续。实际上这就是OOP和泛函编程概念之间的区别:泛函编程的风格就是在一些管子里进行数据读取,没有必要先取出来。看看如何使用以上函数吧:
r = divide(3.3, 5.0) getOrElse raio(...)
简单明了许多吧。那下面我们就专注于这个Option的实现吧。既然相像只有一个元素的List,那么就不需要哪些复杂的什么左右折叠算法了:
trait Option[+A] {
def map[B](f: A => B): Option[B] = this match {
case None => None
case Some(a) => Some(f(a))
}
def flatMap[B](f: A => Option[B]): Option[B] = this match {
case None => None
case Some(a) => f(a)
}
def filter(f: A => Boolean): Option[A] = this match {
case Some(a) if (f(a)) => this
case _ => None
}
def getOrElse[B >: A](default: => B): B = this match {
case None => default
case Some(a) => a
}
def orElse[B >: A](ob: => Option[B]): Option[B] = this match {
case None => ob
case _ => this
}
}
注意:上面的[B >: A]是指类型B是类型A的父类,结合+A变形,Option[B]就是Option[A]的父类:如果A是Apple,那么B可以是Fruit,那么上面的默认值类型就可以是Fruit,或者是Option[Fruit]了。=> B表示输入参数B是拖延计算的,意思是在函数内部真正参考(refrence)这个参数时才会对它进行计算。
下面通过一些使用案例来说明:
//在管子里相加。结果还是保留在管子内
Some(2) map {_ + 3} //> res0: ch4.exx.Option[Int] = Some(5)
val none = None: Option[Int] //> none : ch4.exx.Option[Int] = None
//可以直接使用None而不会出异常
none map {_ + 3} //> res1: ch4.exx.Option[Int] = None //在管子里相加。结果还是保留在管子内
Some(2) flatMap { x => Some(x + 3)} //> res2: ch4.exx.Option[Int] = Some(5)
//可以直接使用None而不会出异常
none flatMap { x => Some(x + 3)} //> res3: ch4.exx.Option[Int] = None Some(2) getOrElse 5 //> res4: Int = 2
none getOrElse 5 //> res5: Int = 5
Some(2) orElse Some(5) //> res6: ch4.exx.Option[Int] = Some(2)
none orElse Some(5) //> res7: ch4.exx.Option[Int] = Some(5)
Option的内部函数组合例子:
def flatMap_1[B](f: A => Option[B]): Option[B] = {
map(f) getOrElse None
// map(f) >>>> Option[Option[B]]
// 如果 Option[B] = X >>>> getOrElse Option[X] = X = Option[B]
}
def orElse_1[B >: A](ob: => Option[B]): Option[B] = {
map(Some(_)) getOrElse None
//this[Option[A]] Some(_) >>>> Option[A]
//map(Some(_)) >>>> Option[Option[A]]
}
def filter_1(f: A => Boolean): Option[A] = {
flatMap(a => if(f(a)) Some(a) else None)
}
Option数据类型使编程者无须理会函数的异常,可以用简洁的语法专注进行函数组合(function composition)。普及使用Option变成了泛函编程的重要风格。Scala是一种JVM编程语言,因而在用Scala编程时可能会调用大量的java库函数。那么我们如何保证在调用现有java库的同时又可以不影响泛函编程风格呢?我们需不需要在使用java函数时用null和Exception而在Scala中就用Option呢?答案是否定的!通过泛函编程的函数组合我们可以在不改变java源代码的情况下实现对java库函数的“升格”(lifting)。实际上我们现在泛函编程中的风格要求是在调用某个函数时,这个函数要能接受Option类型传入参数及返回Option类型值。用函数类型来表达就是:把 A => B 这样的函数编程“升格”成 Option[A] => Option[B]这样的函数:
def lift[A,B](f: A => B): (Option[A] => Option[B]) = _ map f
Woo,简直太神奇了。先从类型匹配上分析:map(f) >>> Option[B]。这个占位符 _ 在这里代表输入参数,就是 this >>>>>> Opption[A]。所以类型匹配。实际上这个函数表达形式先明确了最后生成的结果函数是:给一个Option,返回一个Option,这不是典型的函数文本(lambda function)描述吗:oa => oa map f >>> _ map f 。
我们还是用上面那个简单的divide例子吧:divide(x,y)需要两个输入参数,我们可以再造个更简单的,一个输入参数的例子:9 除以任何double y:
def divide9(y: Double): Double ={
9 / y
} //> divide9: (y: Double)Double
就是一个简单的 A => B,我们可以试试使用:
divide9(2.0) //> res0: Double = 4.5
divide9(3.3) //> res1: Double = 2.7272727272727275
传入一个Double参数, 返回Double值。
把divide9“升格”后再试试:
val lifted = lift[Double,Double](divide9) //> lifted : ch4.exx.Option[Double] => ch4.exx.Option[Double] = <function1>
lifted(Some(2.0)) //> res2: ch4.exx.Option[Double] = Some(4.5)
lifted(None) //> res3: ch4.exx.Option[Double] = None
divide9升格成lifted, 传入lifted一个Option, 返回一个Option。正是我们期望的结果。
再试复杂一点的:两个、三个参数函数升格:
// 用for comprehension 两个参数
def lift2[A,B,C](f:(A,B) => C):(Option[A],Option[B]) => Option[C] = {
(oa: Option[A], ob: Option[B]) => for {
aa <- oa
bb <- ob
} yield f(aa,bb)
}
//用 flatMap款式 三个参数
def lift3[A,B,C,D](f:(A,B,C) => D):(Option[A],Option[B],Option[C]) => Option[D] ={
(oa: Option[A], ob: Option[B], oc: Option[C]) =>
oa.flatMap(aa => ob.flatMap(bb => oc.map ( cc => f(aa,bb,cc) )))
}
测试使用结果:
def divide(x: Double,y: Double): Double ={
x / y
} //> divide: (x: Double, y: Double)Double
val lifted2 = lift2(divide) //> lifted2 : (ch4.exx.Option[Double], ch4.exx.Option[Double]) => ch4.exx.Opti
//| on[Double] = <function2>
lifted2(Some(9),Some(2.0)) //> res2: ch4.exx.Option[Double] = Some(4.5) def divThenMul(x: Double, y: Double, z: Double): Double = {
x / y * z
} //> divThenMul: (x: Double, y: Double, z: Double)Double
val lifted3 = lift3(divThenMul) //> lifted3 : (ch4.exx.Option[Double], ch4.exx.Option[Double], ch4.exx.Option[
//| Double]) => ch4.exx.Option[Double] = <function3>
lifted3(Some(9.0),Some(2.0),Some(5)) //> res3: ch4.exx.Option[Double] = Some(22.5)
这显示了泛函编程函数组合的优雅但强大特性。
下面看看Option的函数组合(function composition):map2用一个函数f在Option管道内把两个Option合并起来:
def map2[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = (a,b) match {
case (None, _) => None
case (_, None) => None
case (Some(x),Some(y)) => Some(f(x,y))
}
//因为Option有 map 和 flatMap, 可以使用 for comprehensiob
def map2_2[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = {
for {
aa <- a
bb <- b
} yield f(aa,bb)
}
//以上的for comprehension可以化为flatMap和Map如下:
def map2_1[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = {
a flatMap(aa => b map(bb => f(aa,bb)))
}
在实现了map和flatMap两个函数基础上,以上展示了for语法糖(syntatic sugar)的用法。
下面的例子是针对List里面的Option,List[Option[A]]来操作的。既然涉及到List,那么就可能涉及到折叠算法了。
下面这个例子:把List[Option[A]]转化成Option[List[A]],数据示范:List(Some("Hello"),Some("World"))变成 Some(List("Hello","World")。一旦list里包含了None值则返回None:List(Some("Hello"),None,Some("World"))直接变成None:
def sequence[A](a: List[Option[A]]): Option[List[A]] = a match {
case Nil => Some(Nil)
case h :: t => h flatMap(hh => sequence(t) map(hh :: _))
}
def sequence_1[A](a: List[Option[A]]): Option[List[A]] = {
a.foldRight[Option[List[A]]](Some(Nil))((x,y) => map2(x,y)(_ :: _))
}
以上使用了map2:一个把两个Option结合起来的函数。这次提供了一个创建List的操作函数。测试一下结果:
val lo = List(Some("Hello"),Some("World"),Some("!"))
//> lo : List[ch4.exx.Some[String]] = List(Some(Hello), Some(World), Some(!))
val lwn = List(Some("Hello"),None,Some("World"),Some("!"))
//> lwn : List[Product with Serializable with ch4.exx.Option[String]] = List(S
//| ome(Hello), None, Some(World), Some(!)) sequence(lo) //> res0: ch4.exx.Option[List[String]] = Some(List(Hello, World, !))
sequence(lwn) //> res1: ch4.exx.Option[List[String]] = None
对于涉及List的情况,另外一个函数traverse也值得注意。下面是traverse的设计:
// 用递归方式
def traverse[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = {
as match {
case Nil => Some(Nil)
case h :: t => map2(f(h),traverse(t)(f))(_ :: _)
}
}
// 用右折叠foldRight
def traverse_1[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = {
as.foldRight[Option[List[B]]](Some(Nil))((h,t) => map2(f(h),t)(_ :: _))
}
traverse的功能是使用函数f对List as里的所有元素进行作用,然后生成Option[List[B]]。看看使用结果:
val list = List("Hello","","World","!") //> list : List[String] = List(Hello, "", World, !)
traverse(list)( a => Some(a) ) //> res0: ch4.exx.Option[List[String]] = Some(List(Hello, , World, !))
OK, Option的介绍就到此了。
泛函编程(9)-异常处理-Option的更多相关文章
- 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink
上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...
- 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process
IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...
- 泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(25)-泛函数据类型-Monad-Applicative
上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...
- 泛函编程(24)-泛函数据类型-Monad, monadic programming
在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...
- 泛函编程(23)-泛函数据类型-Monad
简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...
- 泛函编程(21)-泛函数据类型-Monoid
Monoid是数学范畴理论(category theory)中的一个特殊范畴(category).不过我并没有打算花时间从范畴理论的角度去介绍Monoid,而是希望从一个程序员的角度去分析Monoid ...
- 泛函编程(14)-try to map them all
虽然明白泛函编程风格中最重要的就是对一个管子里的元素进行操作.这个管子就是这么一个东西:F[A],我们说F是一个针对元素A的高阶类型,其实F就是一个装载A类型元素的管子,A类型是相对低阶,或者说是基础 ...
随机推荐
- [转]jQuery.Autocomplete实现自动完成功能(详解)
本篇文章除了介绍jquery.autocomplete基本参数外,主要说明jquery.autocomplete的数据源的格式问题. 1.jquery.autocomplete参考地址 htt ...
- 查询反模式 - GroupBy、HAVING的理解
为了最简单地说明问题,我特地设计了一张这样的表. 一.GROUP BY单值规则 规则1:单值规则,跟在SELECT后面的列表,对于每个分组来说,必须返回且仅仅返回一个值. 典型的表现就是跟在SELEC ...
- CodeWarrior环境下中断使用
对于飞思卡尔CodeWarrior的中断使用,一般有3种方法: 1.把#pragma TRAP_PROC放在中断程序前面,并把中断向量表放到*.prm. 例如: #pragma TRAP_PROC v ...
- mshadow笔记
矩阵维度表示和正常相反. a[2][3],行2列3,a.shape.shape_[0]=3,a.shape.shape_[1]=2. pred.Resize( Shape2( batch_size, ...
- MSSQL获得表的字段名称及其参数
SELECT ColumnsName = c.name, [Description] = ex.value, ColumnType = t.name, [Length]=c.max_length FR ...
- VS2010 发布web项目 问题
载:http://www.cnblogs.com/shaocm/archive/2012/08/10/2632116.html
- 获取ACCESS数据库中自增变量的值
/// <summary> /// 获取自增变量值 /// </summary> /// <returns>自增变量值</returns> public ...
- XPS 15 9530使用Windows10频繁发生Intel HD Graphics 4600驱动奔溃的一种解决方法
本人使用XPS 15 9530.集成显卡为Intel HD Graphics 4600.操作系统Windows 10 Pro,使用过程当中经常会发生集成显卡奔溃的问题,错误提示如下: Display ...
- java攻城狮之路(Android篇)--BroadcastReceiver&Service
四大组件:activity 显示. contentProvider 对外暴露自己的数据给其他的应用程序.BroadcastReceiver 广播接收者,必须指定要接收的广播类型.必须明确的指定acti ...
- SNF开发平台WinForm之十四-站内发送系统信息-SNF快速开发平台3.3-Spring.Net.Framework
1运行效果: 2开发实现: .组装站内信息发送实体对象. SNFService SNFService = new SNFService(); if (this.ucUser.SelectedIds ! ...