泛函编程(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的介绍就到此了。

时间: 2024-11-10 05:34:55

泛函编程(9)-异常处理-Option的相关文章

泛函编程(21)-泛函数据类型-Monoid

    Monoid是数学范畴理论(category theory)中的一个特殊范畴(category).不过我并没有打算花时间从范畴理论的角度去介绍Monoid,而是希望从一个程序员的角度去分析Monoid以及它在泛函编程里的作用.从这个思路出发我们很自然得出Monoid就是一种数据类型,或者是一种在泛函编程过程中经常会遇到的数据类型:当我们针对List或者loop进行一个数值的积累操作时我们就会使用到Monoid.实际上Monoid就是List[A] => A的抽象模型.好了,我们就不要越描

泛函编程(25)-泛函数据类型-Monad-Applicative

    上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Monad实例,就可以使用Monad组件库像for-comprehension这样特殊的.Monad具备的泛函式数据结构内部的按序计算运行流程.针对不同的数据类型,flatMap+unit组件实现方式会有所不同,这是因为flatMap+unit代表着承载数据类型特别的计算行为.之前我们尝试了Li

泛函编程(6)-数据结构-List基础

    List是一种最普通的泛函数据结构,比较直观,有良好的示范基础.List就像一个管子,里面可以装载一长条任何类型的东西.如需要对管子里的东西进行处理,则必须在管子内按直线顺序一个一个的来,这符合泛函编程的风格.与其它的泛函数据结构设计思路一样,设计List时先考虑List的两种状态:空或不为空两种类型.这两种类型可以用case class 来表现: 1 trait List[+A] {} 2 case class Cons[+A](head: A, tail: List[A]) exte

泛函编程(23)-泛函数据类型-Monad

  简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可以与任何类型的Monad实例一样在泛函编程中共同提供一套通用的泛函编程方式.所以有人把泛函编程视作Monadic Programming也不为过之.那么,具体什么是Monad呢?     在前面我们讨论过Monoid,我们说过它是一个特殊的范畴(Category),所有数据类型的Monoid实例

泛函编程(27)-泛函编程模式-Monad Transformer

  经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative,Traversable,Monad也是我们将来进入实际泛函编程的必需.在前面对这些数据类型的探讨中我们发现: 1.Monoid的主要用途是在进行折叠(Foldable)算法时对可折叠结构内元素进行函数施用(function application). 2.Functor可以对任何高阶数据类型F[

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

 由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码(impure code)函数中的纯代码部分(pure code)抽离出来形成独立的另一个纯函数.我们通过代码抽离把不纯代码逐步抽离向外推并在程序里形成一个纯代码核心(pure core).这样我们就可以顺利地在这个纯代码核心中实现函数组合.IO Monad就是泛函编程处理副作用代码的一种手段.我们

泛函编程(36)-泛函Stream IO:IO数据源-IO Source &amp; Sink

 上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接起来形成一串具备多项数据处理功能的完整IO过程.但合成的IO过程两头输入端则需要接到一个数据源,而另外一端则可能会接到一个数据接收设备如文件.显示屏等.我们在这篇简单地先介绍一下IO数据源Source和IO数据接收端Sink. 我们先用一个独立的数据类型来代表数据源Source进行简单的示范说明,

泛函编程(24)-泛函数据类型-Monad, monadic programming

   在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个明确的答案吗?我们先从上节设计的Monad组件库中的一些基本函数来加深一点对Monad的了解: 1 trait Monad[M[_]] extends Functor[M] { 2 def unit[A](a: A): M[A] 3 def flatMap[A,B](ma: M[A])(f:

泛函编程(35)-泛函Stream IO:IO处理过程-IO Process

    IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数据源可能是一串键盘字符.鼠标位置坐标.文件字符行.数据库纪录等.如何实现泛函模式的Stream IO处理则是泛函编程不可或缺的技术. 首先,我们先看一段较熟悉的IO程序: 1 import java.io._ 2 def linesGt4k(fileName: String): IO[Boo