泛函编程(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的数据类型设计搬过来试试:

1   trait Option[+A] 
2   case object None extends Option[Nothing]
3   case class Some[+A](value: A) extends Option[A]

这简直跟List一模样嘛。当然,结构是一样的,但因为Option最多可以有一个元素,所有的操作函数将会简洁的多。

那么为什么要增加一种数据类型?Option又是用来干什么的呢?

我们先拿个超简单的java例子来示范:

 1 java code
 2    double divide(double x, double y) 
 3    {
 4        double ratio;
 5        try {
 6         z = x / y;
 7        } catch (Exception e) {
 8          //bala bala ,,,
 9          return ?????
10        }
11        return ratio;
12    }

在写这段java程序时一个疑问立即跳了出来:如果出现了错误时这个函数该返回什么呢?函数申明divide返回double,但在发生运算错误后我们不能返回任何double值,任何double值都不正确。唯一选择就是通过异常处理(Exception Handling)来解决了。那是不是意味着这个函数的所有用户都必须自己增加一段代码去处理异常了呢?那么每个用户都必须这么写:

1 java code
2        try {
3            r = divide(x,y);
4                    //bala bala ...
5        } catch (Exception e) {
6          //bala bala ,,,
7         // bala bala ...
8        }
这样做勉强可以继续编程,但最终程序变的一塌糊涂,增加了许多无谓的代码,也臃肿了整改程序,增加了编程人员阅读理解的难度。泛函编程的这个Option数据类型正是为解决这样的问题而增加的。如果以上问题用Scala来编写的话:
1  def divide(x: Double, y: Double): Option[Double] = {
2       try {
3           Some(x/y)
4       } catch {
5           case e: Exception => None
6       }
7   }

首先,不用再头疼该返回什么值了:出问题就直接返回None。不过使用者必须从Option这个管子里先把值取出来,看起来好像又多了一道手续。实际上这就是OOP和泛函编程概念之间的区别:泛函编程的风格就是在一些管子里进行数据读取,没有必要先取出来。看看如何使用以上函数吧:

1 r = divide(3.3, 5.0) getOrElse raio(...)

简单明了许多吧。那下面我们就专注于这个Option的实现吧。既然相像只有一个元素的List,那么就不需要哪些复杂的什么左右折叠算法了:

 1   trait Option[+A] {
 2       def map[B](f: A => B): Option[B] = this match {
 3           case None => None
 4           case Some(a) => Some(f(a))
 5       }
 6       def flatMap[B](f: A => Option[B]): Option[B] = this match {
 7           case None => None
 8           case Some(a) => f(a)
 9       }
10       def filter(f: A => Boolean): Option[A] = this match {
11           case Some(a) if (f(a)) => this
12           case _ => None
13       }
14       def getOrElse[B >: A](default: => B): B = this match {
15           case None => default
16           case Some(a) => a
17       }
18       def orElse[B >: A](ob: => Option[B]): Option[B] = this match {
19           case None => ob
20           case _ => this
21       }
22   }

注意:上面的[B >: A]是指类型B是类型A的父类,结合+A变形,Option[B]就是Option[A]的父类:如果A是Apple,那么B可以是Fruit,那么上面的默认值类型就可以是Fruit,或者是Option[Fruit]了。=> B表示输入参数B是拖延计算的,意思是在函数内部真正参考(refrence)这个参数时才会对它进行计算。

下面通过一些使用案例来说明:

 1   //在管子里相加。结果还是保留在管子内
 2   Some(2) map {_ + 3}                             //> res0: ch4.exx.Option[Int] = Some(5)
 3   val none = None: Option[Int]                    //> none  : ch4.exx.Option[Int] = None
 4   //可以直接使用None而不会出异常
 5   none map {_ + 3}                                //> res1: ch4.exx.Option[Int] = None
 6   
 7   //在管子里相加。结果还是保留在管子内
 8   Some(2) flatMap { x => Some(x + 3)}             //> res2: ch4.exx.Option[Int] = Some(5)
 9   //可以直接使用None而不会出异常
10   none flatMap { x => Some(x + 3)}                //> res3: ch4.exx.Option[Int] = None
11   
12   Some(2) getOrElse 5                             //> res4: Int = 2
13   none getOrElse 5                                //> res5: Int = 5
14   Some(2) orElse Some(5)                          //> res6: ch4.exx.Option[Int] = Some(2)
15   none orElse Some(5)                             //> res7: ch4.exx.Option[Int] = Some(5)

Option的内部函数组合例子:

 1       def flatMap_1[B](f: A => Option[B]): Option[B] = {
 2           map(f) getOrElse None
 3           // map(f) >>>> Option[Option[B]] 
 4           // 如果 Option[B] = X >>>> getOrElse Option[X] = X = Option[B]
 5       }
 6       def orElse_1[B >: A](ob: => Option[B]): Option[B] = {
 7           map(Some(_)) getOrElse None
 8           //this[Option[A]] Some(_) >>>> Option[A] 
 9           //map(Some(_)) >>>> Option[Option[A]]
10       }
11       def filter_1(f: A => Boolean): Option[A] = {
12           flatMap(a => if(f(a)) Some(a) else None)
13       }

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]这样的函数:

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

1   def divide9(y: Double): Double ={
2       9 / y
3   }                                               //> divide9: (y: Double)Double

就是一个简单的 A => B,我们可以试试使用:

 divide9(2.0)                                    //> res0: Double = 4.5
 divide9(3.3)                                    //> res1: Double = 2.7272727272727275

传入一个Double参数, 返回Double值。

把divide9“升格”后再试试:

1  val lifted = lift[Double,Double](divide9)       //> lifted  : ch4.exx.Option[Double] => ch4.exx.Option[Double] = <function1>
2   lifted(Some(2.0))                               //> res2: ch4.exx.Option[Double] = Some(4.5)
3   lifted(None)                                    //> res3: ch4.exx.Option[Double] = None

divide9升格成lifted, 传入lifted一个Option, 返回一个Option。正是我们期望的结果。 再试复杂一点的:两个、三个参数函数升格:

 1         // 用for comprehension 两个参数
 2       def lift2[A,B,C](f:(A,B) => C):(Option[A],Option[B]) => Option[C] = {
 3       (oa: Option[A], ob: Option[B]) => for {
 4           aa <- oa
 5           bb <- ob
 6       } yield f(aa,bb)
 7       }
 8       //用    flatMap款式  三个参数
 9       def lift3[A,B,C,D](f:(A,B,C) => D):(Option[A],Option[B],Option[C]) => Option[D] ={
10           (oa: Option[A], ob: Option[B], oc: Option[C]) =>
11               oa.flatMap(aa => ob.flatMap(bb => oc.map ( cc => f(aa,bb,cc) )))
12       }

测试使用结果:

 1   def divide(x: Double,y: Double): Double ={
 2          x / y
 3   }                                               //> divide: (x: Double, y: Double)Double
 4   val lifted2 = lift2(divide)                     //> lifted2  : (ch4.exx.Option[Double], ch4.exx.Option[Double]) => ch4.exx.Opti
 5                                                   //| on[Double] = <function2>
 6   lifted2(Some(9),Some(2.0))                      //> res2: ch4.exx.Option[Double] = Some(4.5)
 7   
 8   def divThenMul(x: Double, y: Double, z: Double): Double = {
 9       x / y * z
10   }                                               //> divThenMul: (x: Double, y: Double, z: Double)Double
11   val lifted3 = lift3(divThenMul)                 //> lifted3  : (ch4.exx.Option[Double], ch4.exx.Option[Double], ch4.exx.Option[
12                                                   //| Double]) => ch4.exx.Option[Double] = <function3>
13   lifted3(Some(9.0),Some(2.0),Some(5))            //> res3: ch4.exx.Option[Double] = Some(22.5)

这显示了泛函编程函数组合的优雅但强大特性。

下面看看Option的函数组合(function composition):map2用一个函数f在Option管道内把两个Option合并起来:

 1       def map2[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = (a,b) match {
 2           case (None, _) => None
 3           case (_, None) => None
 4           case (Some(x),Some(y)) => Some(f(x,y))
 5       }
 6       //因为Option有 map 和 flatMap, 可以使用 for comprehensiob
 7       def map2_2[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = {
 8           for {
 9               aa <- a
10               bb <- b
11           } yield f(aa,bb)
12       }
13       //以上的for comprehension可以化为flatMap和Map如下:
14       def map2_1[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = {
15           a flatMap(aa => b map(bb => f(aa,bb)))
16       }

在实现了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:

1       def sequence[A](a: List[Option[A]]): Option[List[A]] = a match {
2           case Nil => Some(Nil)
3           case h :: t => h flatMap(hh => sequence(t) map(hh :: _))
4       }
5       def sequence_1[A](a: List[Option[A]]): Option[List[A]] = {
6           a.foldRight[Option[List[A]]](Some(Nil))((x,y) => map2(x,y)(_ :: _))
7       }

以上使用了map2:一个把两个Option结合起来的函数。这次提供了一个创建List的操作函数。测试一下结果:

1 val lo = List(Some("Hello"),Some("World"),Some("!"))
2                              //> lo  : List[ch4.exx.Some[String]] = List(Some(Hello), Some(World), Some(!))
3   val lwn = List(Some("Hello"),None,Some("World"),Some("!"))
4                              //> lwn  : List[Product with Serializable with ch4.exx.Option[String]] = List(S
5                              //| ome(Hello), None, Some(World), Some(!))
6   
7   
8   sequence(lo)               //> res0: ch4.exx.Option[List[String]] = Some(List(Hello, World, !))
9   sequence(lwn)              //> res1: ch4.exx.Option[List[String]] = None

对于涉及List的情况,另外一个函数traverse也值得注意。下面是traverse的设计:

 1       // 用递归方式
 2       def traverse[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = {
 3           as match {
 4               case Nil => Some(Nil)
 5               case h :: t => map2(f(h),traverse(t)(f))(_ :: _)
 6           }
 7       }
 8       // 用右折叠foldRight
 9       def traverse_1[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = {
10            as.foldRight[Option[List[B]]](Some(Nil))((h,t) => map2(f(h),t)(_ :: _))
11       }

traverse的功能是使用函数f对List as里的所有元素进行作用,然后生成Option[List[B]]。看看使用结果:

1   val list = List("Hello","","World","!")         //> list  : List[String] = List(Hello, "", World, !)
2   traverse(list)( a => Some(a) )                  //> res0: ch4.exx.Option[List[String]] = Some(List(Hello, , World, !))

OK, Option的介绍就到此了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java 成神之路

Java 动态代理 原理解析

28110
来自专栏维C果糖

Guava 指南 之「通用 Object 方法」

通用 Object 方法 equals 当你的对象含有的多个字段可能为null的时候,实现Object.equals会很痛苦,因为你不得不分别对它们进行null...

19310
来自专栏码匠的流水账

聊聊storm trident的state

storm-2.0.0/storm-client/src/jvm/org/apache/storm/trident/state/StateType.java

914
来自专栏陈树义

Java ConcurrentModificationException异常原因和解决方法

Java ConcurrentModificationException异常原因和解决方法   在前面一篇文章中提到,对Vector、ArrayList在迭代的...

3624
来自专栏面朝大海春暖花开

@Autowired和@Resource的区别

@Autowired 这个是spring的注解    org.springframework.beans.factory.annotation.Autowire...

1552
来自专栏DT乱“码”

正则判断工具类

package com.gulf.utils; import java.text.ParseException; import java.text.Simpl...

19810
来自专栏JAVA技术站

JFinal 参数校验插件扩展,让后台参数校验像js一样方式好用

email=^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$,"邮箱格式不正确" chinese=^[...

1602
来自专栏烙馅饼喽的技术分享

用ECMAScript4 ( ActionScript3) 实现Unity的热更新 -- 操作符重载和隐式类型转换

C#中,某些类型会定义隐式类型转换和操作符重载。Unity中,有些对象也定义了隐式类型转换和操作符重载。典型情况有:UnityEngine.Object。Uni...

2677
来自专栏Java 技术分享

JDBC(MySQL)一周学习总结(一)

3138
来自专栏lgp20151222

SSH上一个随笔的基础上添加上hibernate支持

熟悉的pom.xml其中lo4g和slf4j这两个包第一眼看上去有点莫名奇妙,我也是这么觉得的,实际作用是在后台输出sql语句,不导入hibernate就会报错...

601

扫码关注云+社区