泛函编程(10)-异常处理-Either

上节我们介绍了新的数据类型Option:一个专门对付异常情况出现时可以有一致反应所使用的数据类型。Option可以使编程人员不必理会出现异常后应该如何处理结果,他只是获得了一个None值,但这个None值与他所期待的类型是一致的,他可以继续用处理这种类型数据的方法使用这个结果。不过遗憾的是我们通过None值只能知道某个计算没能得出结果,但到底发生了什么事Option并没有提供任何提示。这样我们也就无法向用户提供贴切的系统错误或着操作失误信息了。

这样我们就需要在Option的基础上添加一个扩展功能的新数据类型,让它可以返回一些异常描述:Either。可以想象Either在返回None的同时还要包含一个返回值,用来描述异常。那么这个None的形式就变成了None(e)了。我们先看看Eigher的框架设计:

1   trait Either[+E,+A]
2   case class Left[+E](value: E) extends Either[E,Nothing]
3   case class Right[+A](value: A) extends Either[Nothing,A]

以上可见Either需要处理两个类型E和A:E代表异常类型,A代表计算类型。与Option一样,Either也有两种状态:Left代表无法完成计算,返回值E是对异常情况的描述、Right则代表计算正常完成,返回计算结果A。从英文解释,Either不是Right就是Left。这种情况被称为类型的“不联合性”(disjoint union)。

提出了Either的基本描述后开始数据类型操作函数的实现:

 1       def map[B](f: A => B): Either[E,B] = this match {
 2           case Right(a) => Right(f(a))
 3           case Left(e) => Left(e)
 4       }
 5       def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = this match {
 6           case Left(e)  => Left(e)
 7           case Right(a) => f(a)
 8       }
 9       def orElse[EE >: E, AA >: A](default: Either[EE, AA]): Either[EE, AA] = this match {
10           case Left(_) => default
11           case Right(a) => Right(a)
12       }

还是由于Either这种类型的管子里只能存一个元素,所以操作函数的实现比较直接简单:用类型匹配和递归算法就行了。

在以下的函数中我们可以用一个函数 (A,B) => C 把两个Either[A],Either[B]组合成Either[C]:

 1       //用递归算法
 2       def map2[EE >: E, B, C](b: Either[EE, B])(f: (A,B) => C): Either[EE, C] = (this,b) match {
 3           case (Left(e),_) => Left(e)
 4           case (_, Left(e)) => Left(e)
 5           case (Right(a),Right(b)) => Right(f(a,b))
 6       }
 7       //用for comprehension
 8         def map2_1[EE >: E, B, C](b: Either[EE, B])(f: (A,B) => C): Either[EE, C] = {
 9             for {
10                 aa <- this
11                 bb <- b
12             } yield f(aa,bb)
13         }
14         //用 flatMap写
15         def map2_2[EE >: E, B, C](b: Either[EE, B])(f: (A,B) => C): Either[EE, C] = {
16             flatMap(aa => b map(bb => f(aa,bb)))
17         }

考虑map2时并不复杂:由于我只有一个利用低阶的函数(A,B) =??? ,我必须想办法把Either管子里的那个元素取出来计算完后塞到一个新的Either管子里去。以上我们已经实现了map,flatMap我们可以使用for comprehension来实现:

aa <- a: Either - 从Either管子取出元素

yield 产生新的Either。map2_1是for comprehension的直接写法。

由于我们有map和flatMap,我们可以试着用用Either:

 1  case class Employee(name: String, age: Int, salary: Double)
 2   for {
 3     age <- Right(42)
 4     name <- Left("Invalid Name!")
 5     salary <- Right(10000.00)
 6   } yield Employee(name,age,salary)               //> res0: ch4.either.Either[String,ch4.either.Employee] = Left(Invalid Name!)
 7   for {
 8     age <- Right(42)
 9     name <- Right("Jonny Cash!")
10     salary <- Right(10000.00)
11   } yield Employee(name,age,salary)               //> res1: ch4.either.Either[Nothing,ch4.either.Employee] = Right(Employee(Jonny
12                                                   //|  Cash!,42,10000.0))

可以看出在以上三个动作中(age,name,salary)如果其中任何一个出现了异常Left,结果就会是Left了。

当然,我们还是有可能对一个系列的Either类型值进行计算的,所以sequence,traverse这两个函数总是会用到的:

 1         //用递归算法,用f把元素升格成Either后再用map2把连续的两个元素连接起来
 2       def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] = es match {
 3            case Nil => Right(Nil)
 4            case h :: t => (f(h) map2 traverse(t)(f))(_ :: _)
 5       }
 6       //用foldRight实现,用f把元素升格成Either后再用map2把连续的两个元素连接起来
 7       def traverse_1[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] = {
 8           es.foldRight[Either[E, List[B]]](Right(Nil))((h,t) => f(h).map2(t)(_ :: _))
 9       }
10       def sequence[E,A](es: List[Either[E,A]]): Either[E,List[A]] = es match {
11           case Nil => Right(Nil)
12           case h :: t => (h map2 sequence(t))(_ :: _)
13       }
14       def sequence_1[E,A](es: List[Either[E,A]]): Either[E,List[A]] = {
15           traverse(es)(x => x)
16       }

来个实际点的例子:

 1  case class Name(value: String)
 2   case class Age(value: Int)
 3   case class Person(name: Name, age: Age)
 4   def mkName(name: String): Either[String, Name] = {
 5       if (name == "" || name == null) Left("Invalid Name")
 6       else Right(Name(name))
 7   }                                               //> mkName: (name: String)ch4.either.Either[String,ch4.either.Name]
 8   def mkAge(age: Int): Either[String,Age] = {
 9       if ( age < 0 ) Left("Invalid age")
10       else Right(Age(age))
11   }                                               //> mkAge: (age: Int)ch4.either.Either[String,ch4.either.Age]
12   def mkPerson(name: String, age: Int): Either[String,Person] = {
13       mkName(name).map2(mkAge(age))(Person(_,_))
14   }                                               //> mkPerson: (name: String, age: Int)ch4.either.Either[String,ch4.either.Perso
15                                                   //| n]
16
17   mkPerson("Tiger",18)                            //> res2: ch4.either.Either[String,ch4.either.Person] = Right(Person(Name(Tiger
18                                                   //| ),Age(18)))
19   mkPerson("Tiger",-18)                           //> res3: ch4.either.Either[String,ch4.either.Person] = Left(Invalid age)
20   mkPerson("",-1)                                 //> res4: ch4.either.Either[String,ch4.either.Person] = Left(Invalid Name)

mkPerson输入参数正确时返回Right。任何参数错误返回Left。不过如果两个参数都是错误的话就只能返回其中一个提示信息了。我们可以修改map2来获取全部信息:

1         def map2_s[B, C](b: Either[String, B])(f: (A,B) => C): Either[String, C] = (this,b) match {
2           case (Left(e),Left(ee)) => Left(e+ee)
3           case (_, Left(e)) => Left(e)
4           case (Left(e:String), _) => Left(e)
5           case (Right(a),Right(b)) => Right(f(a,b))
6
7         }

注意:我们必须明确类型E为String,这样才能把两个数据接起来,因为我们不知道如何连接类型E。看看使用新版本后的结果:

 1   def mkPerson(name: String, age: Int): Either[String,Person] = {
 2       mkName(name).map2_s(mkAge(age))(Person(_,_))
 3   }                                               //> mkPerson: (name: String, age: Int)ch4.either.Either[String,ch4.either.Perso
 4                                                   //| n]
 5
 6   mkPerson("Tiger",18)                            //> res2: ch4.either.Either[String,ch4.either.Person] = Right(Person(Name(Tiger
 7                                                   //| ),Age(18)))
 8   mkPerson("Tiger",-18)                           //> res3: ch4.either.Either[String,ch4.either.Person] = Left(Invalid age)
 9   mkPerson("",-1)                                 //> res4: ch4.either.Either[String,ch4.either.Person] = Left(Invalid NameInvali
10                                                   //| d age)

没错,两个信息都连接起来返回了。

时间: 2024-08-28 07:58:46

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

泛函编程(9)-异常处理-Option

Option是一种新的数据类型.形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里:然后在管子内部对数据进行各种操作.所以Option的数据操作与List很相似.不同的是Option的管子内最多只能存放一个元素,在这个方面Option的数据操作就比List简单的多,因为使用者不必理会数据元素的位置.顺序.Option只有两种状态:包含一个任何类型的元素或者为空.或者这样讲:一个Option实例包含 0 或 1 个元素:None代表为空,Some(x)代表包含一个任意类型的

泛函编程(17)-泛函状态-State In Action

对OOP编程人员来说,泛函状态State是一种全新的数据类型.我们在上节做了些介绍,在这节我们讨论一下State类型的应用:用一个具体的例子来示范如何使用State类型.以下是这个例子的具体描述: 模拟一个自动糖果贩售机逻辑:贩售机有两种操作方法:投入硬币和扭动出糖旋钮.贩售机可以处于锁定和放开两种状态.模拟运作跟踪贩售机内当前的糖果和硬币数量.贩售机的操作逻辑要求如下: 1.如果机内有糖的话,投入硬币贩售机从锁定状态进入放开状态 2.在放开状态下扭动旋钮贩售机放出一块糖果后自动进入锁定状态 3

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

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

泛函编程(14)-try to map them all

虽然明白泛函编程风格中最重要的就是对一个管子里的元素进行操作.这个管子就是这么一个东西:F[A],我们说F是一个针对元素A的高阶类型,其实F就是一个装载A类型元素的管子,A类型是相对低阶,或者说是基础的类型.泛函编程风格就是在F内部用对付A类的函数对里面的元素进行操作.但在之前现实编程中确总是没能真正体会这种编程模式畅顺的用法:到底应该在哪里用?怎么用?可能内心里还是没能摆脱OOP的思维方式吧.在前面Stream设计章节里,我们采用了封装形式的数据结构设计,把数据结构uncons放进了特质申明里

泛函编程(15)-泛函状态-随意数产生器

对于OOP程序员来说,泛函状态变迁(functional state transition)是一个陌生的课题.泛函状态变迁是通过泛函状态数据类型(functional state)来实现的.State是一个出现在泛函编程里的类型(type).与其它数据类型一样,State同样需要自身的一套泛函操作函数和组合函数(combinators),我们将在以下章节中讨论有关State数据类型的设计方案. 在正式介绍State类型前,我们先从随意数产生器(RNG: Random Number Generat

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

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

泛函编程(29)-泛函实用结构:Trampoline-不再怕StackOverflow

泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法.比如说flatMap就是一种推进式的递归算法,没了它就无法使用for-comprehension,那么泛函编程也就无法被称为Monadic Programming了.虽然递归算法能使代码更简洁易明,但同时又以占用堆栈(stack)方式运作.堆栈是软件程序有限资源,所以在使用递归算法对大型数据源进行运算时系统往往会出现StackOverflow错误.如果不想办法解决递归算法带来的StackOverflow问题,泛函

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

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

泛函编程(20)-泛函库设计-Further Into Parallelism

上两节我们建了一个并行运算组件库,实现了一些基本的并行运算功能.到现在这个阶段,编写并行运算函数已经可以和数学代数解题相近了:我们了解了问题需求,然后从类型匹配入手逐步产生题解.下面我们再多做几个练习吧. 在上节我们介绍了asyncF,它的类型款式是这样的:asyncF(f: A => B): A => Par[B],从类型款式(type signature)分析,asyncF函数的功能是把一个普通的函数 A => B转成A => Par[B],Par[B]是一个并行运算.也就是说

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

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