泛函编程(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 条评论
登录 后参与评论

相关文章

来自专栏大闲人柴毛毛

稳扎稳打JS——“对象”

一切皆“对象” JS中一切皆“对象” “对象”是属性的集合,而属性又是对象。既然属性又是对象,那么一个对象的属性也可以拥有属性,如: //定义一个函数getN...

3056
来自专栏Java帮帮-微信公众号-技术文章全总结

07.Java变量类型

07.Java变量类型 Java 变量类型 在Java语言中,所有的变量在使用前必须声明。声明变量的基本格式如下: ? 格式说明:type为Java数据类型。i...

3417
来自专栏Bug生活2048

Python自学之路-内置函数说明及实例(四)

如果提供了promat参数,首先将参数值输出到标准的输出,并且不换行。函数读取用户输入的值,将其转换成字符串。

482
来自专栏Golang语言社区

【Go 语言社区】Go语言数组

Go编程语言提供称为数组的数据结构,其可存储相同类型的元素的一个固定大小的连续集合。数组用于存储数据的集合,但它往往是更加有用认为数组作为相同类型的变量的集合。...

34715
来自专栏海天一树

用递归法把二叉树的叶子结点按从左到右的顺序连成一个单链表

一、例子 ? 1.png 上图中的二叉树的叶子结点,按从左到右的顺序连成的单链表如下图所示: ? 2.png 二、定义数据结构 typedef struct t...

3796
来自专栏水击三千

浅谈JavaScript的函数表达式(递归)

  递归函数,在前面的博客中已经简单的介绍了。递归函数是一个通过函数名称在函数内部调用自身的函数。如下: 1 function fac(num){ 2 ...

20810
来自专栏代码世界

Python基础数据类型之字典

 基础数据类型之字典 ps:数据类型划分:可变数据类型和不可变数据类型。 不可变数据类型:元组(tupe)、布尔值(bool)、整数型(int)、字符串(str...

3349
来自专栏WindCoder

在数组中查找次大值,并与最后一个元素交换—C语言

441
来自专栏xingoo, 一个梦想做发明家的程序员

第三章 C++中的C ----《C++编程思想》

1 创建函数 2 执行控制语句   break:退出循环,不再执行循环中的生育语句   continue:停止执行当前的循环,返回到循环的起始处开始新的一轮循环...

1587
来自专栏Python小屋

Python中的循环结构

Python主要有for循环和while循环两种形式的循环结构,多个循环可以嵌套使用,并且还经常和选择结构嵌套使用。while循环一般用于循环次数难以提前确定的...

3296

扫码关注云+社区