Scala函数式编程
异常处理
异常处理的优点:
- 把错误处理逻辑整合集中在一起。
异常处理的缺点:
- 破坏了引用透明,并引入了对上下文的依赖。所以替代模型的简单代换不再适用。 业界最基本的共识是异常处理应该只用于处理错误而不是流程控制。
- 无法检查异常类型是否安全,编译器也不会强制函数的调用者处理被调用函数可能抛出 的异常。编写代码时没有处理的异常赶到程序运行时才被发现。
-
高阶函数以函数用为参数,但是不适用对参数函数可能抛出的异常进行处理。
例:
f(g())
中g()
可能抛出各种异常,f()
中应该如何处理。
用返回值类型代替抛出异常
Scala中可以把抛出的异常设置为任何的类型。比如设定为整数:
def func01(i: Int): Int = { try { val x = 42 + 5 x + ((throw new Exception("fail!")): Int) //异常作为整数返回 } catch { case e: Exception => 42 } } func01(12) // res1: Int 43
用Option表示函数无定义
如果要自己实现Scala中已经有的Option,表示函数并不对所有的参数都有定义:
sealed trait Option[+A] // 有定义 case class Some[+A](get: A) extends Option[A] // 无定义 case object None extends Option[Nothing]
Option基本操作
取值,如果为None
则返回参数指定的default
:
seald trait Option[+A] { // .... def getOrElse[B>:A](default: => B): B = this match { case None => default case Some(a) => a } // .... }
映射操作的返回类型还是Some(A)
:
seald 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] = map(f) getOrElse None // Of course, we can also implement `flatMap` // with explicit pattern matching. def flatMap_1[B](f: A => Option[B]): Option[B] = this match { case None => None case Some(a) => f(a) } // .... }
orElse(ob)
避免对参数ob
求值(除非必要的情况下):
seald trait Option[+A] { // .... def orElse[B>:A](ob: => Option[B]): Option[B] = this map (Some(_)) getOrElse ob /* Again, we can implement this with explicit pattern matching. */ def orElse_1[B>:A](ob: => Option[B]): Option[B] = this match { case None => ob case _ => this } // .... }
过滤不符合的内容:
seald trait Option[+A] { // .... def filter(f: A => Boolean): Option[A] = this match { case Some(a) if f(a) => this case _ => None } /* This can also be defined in terms of `flatMap`. */ def filter_1(f: A => Boolean): Option[A] = flatMap(a => if (f(a)) Some(a) else None) // .... }
Option应用场景
Option类型的链式调用,None
类型的map
、flatMap
、filter
返回的都是None
。
所以链式调用中间有一步的结果为None
会中断调用:
case class Employee(name: String, department: String, manager: String) def lookupByName(name: String): Option[Employee] = name match { case "Joe" => Some(new Employee("Joe", "R&D Dept.", "Jade")) case _ => None } lookupByName("Joe").map(_.department) //> res0: Option[String] = Some(R&D Dept.) lookupByName("Joe").map(_.department).getOrElse("Default Dept.") //> res2: String = R&D Dept. lookupByName("Teo").map(_.department) //> res1: Option[String] = None lookupByName("Teo").map(_.department).getOrElse("Default Dept.") //> res3: String = Default Dept. lookupByName("Joe").map(_.department).filter(_ != "R&D Dept." ).getOrElse("Default Dept.") //> res8: String = Default Dept. lookupByName("Joe").map(_.department).filter(_ != "Accounting Dept." ).getOrElse("Default Dept.") //> res9: String = R&D Dept. lookupByName("Joe").flatMap(_ match {case t => Some(t)}) //> res4: Option[Employee] = Some(Employee(...)) lookupByName("Joe").flatMap(_.department match {case t => Some(t)}) //> res5: Option[String] = Some(R&D Dept.) lookupByName("Teo").flatMap(_ match {case t => Some(t)}) //> res6: Option[Employee] = None lookupByName("Teo").flatMap(_.department match {case t => Some(t)}) //> res7: Option[String] = None
使用flatMap实现方差(variance)函数。如果一个序列的平均值是\(m\), 方差是对序列中的第个元素\(x\)进行\(math.pow(x-m, 2)\)。
方差调用的多个阶段中都有可能失败,运算是遇到第一个失败就中止,
因为None.flatMap(f)
会返回None
,不再调用函数f
:
// 平均值 def mean(xs: Seq[Double]): Option[Double] = if (xs.isEmpty) None else Some(xs.sum / xs.length) def variance(xs: Seq[Double]): Option[Double] = mean(xs) flatMap (m => mean(xs.map(x => math.pow(x - m, 2))))
把Option转回异常
必要的情况下,把Option转回为异常处理也不是不可以:
result.getOrElse(throw new Exception("Fail"))
Option与函数提升
用Option有很多好处,但是很多函数的参数与返回值并不是Option类型的。
lift
函数可以把函数的参数与返回值都包装为Option,即把f: A => B
包装为g: Option[A] => Option[B]
:
lift
函数返回一个新的函数作为返回值,这个新函数以Option[A]
为参数,
这样Option[A].map(f)
的返回值类型就是Option[B]
:
def lift2[A, B](f: A => B): Option[A] => Option[B] = { val newFunction = (t: Option[A]) => { t map f } newFunction } //> lift2: [A, B](f: A => B)Option[A] => Option[B] // 简写一下: def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f //> lift: [A, B](f: A => B)Option[A] => Option[B] val abs = lift(math.abs) //> absO : Option[Int] => Option[Int]
结合异常与提升Option
让异常返回None
:
def parseStringAge(age: String): Int = age.toInt //> parseStringAge: (age: String)Int parseStringAge("33") //> res10: Int = 33 parseStringAge("Hello") //> java.lang.NumberFormatException: ........ def Try[A](a: => A): Option[A] = try Some(a) catch { case e: Exception => None // 这样会丢弃异常信息,以后再改进 } //> Try: [A](a: => A)Option[A] def parseStringAge2(age: String): Option[Int] = { Try(age.toInt) } //> parseStringAge2: (age: String)Option[Int] parseStringAge2("33") //> res11: Option[Int] = Some(33) parseStringAge2("hello") //> res12: Option[Int] = None parseStringAge2("world") //> res13: Option[Int] = None
提升有多个参数的函数
lift
只能提升单个参数的函数,如果函数有多个参数就不能通过lift
提升:
/* 根据年龄与超速罚单的数量评价保险的评分 */ def insuranceRateQuote(age: Int, numberOfSpeedingTickets: Int): Double = ??? //> insuranceRateQuote: (age: Int, numberOfSpeedingTickets: Int)Double
要通过新的工具函数map2
把两个参数都提升为Option
:
def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] = a flatMap (aa => b map (bb => f(aa, bb))) //> map2: [A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C)Option[C] def parseInsuranceRateQuote(age: String, numberOfSpeedingTickets: String): Option[Double] = { val optAge: Option[Int] = Try(age.toInt) val optTick: Option[Int] = Try(numberOfSpeedingTickets.toInt) map2(optAge, optTick)(insuranceRateQuote) } //> parseInsuranceRateQuote: (age: String, //| numberOfSpeedingTickets: String)Option[Double]
合并多个Option为单个Option的列表
sequence
函数把含有多个Option的列表List[Option[A]]
结合为一个Option[List[A]]
如果原来的列表里有一个为None
,那么sequence
函数返回的结果也是None
:
def sequence[A](lst: List[Option[A]]): Option[List[A]] = lst match { case Nil => Some(Nil) case h :: t => h flatMap (hh => sequence(t) map (hh :: _)) } //> sequence: [A](lst: List[Option[A]])Option[List[A]]
还可以通过 foldRight
和 map2
来实现sequence
,在foldRight
里要注明类型为
[Option[List[A]]]
不然会被类型推导错误地推导为Some[Nil.type]
def sequence_1[A](a: List[Option[A]]): Option[List[A]] = a.foldRight[Option[List[A]]](Some(Nil))((x, y) => map2(x, y)(_ :: _)) //> sequence_1: [A](a: List[Option[A]])Option[List[A]]
sequence
仅仅只适合用来合并列表为单个option,如果涉及到map操作,就要遍历两次,
例如下面的字符串转整数,map的时候遍历一遍lst
,合并Option时又要遍历一遍:
def parseInts(lst: List[String]): Option[List[Int]] = sequence(lst map (i => Try(i.toInt)))
定义新的工具traverse
直接在一个遍历过程中map并合并Option:
def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] = a match { case Nil => Some(Nil) case h :: t => map2(f(h), traverse(t)(f))(_ :: _) } //> traverse: [A, B](a: List[A])(f: A => Option[B])Option[List[B]] def traverse_1[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] = { a.foldRight[Option[List[B]]](Some(Nil))((h, t) => map2(f(h), t)(_ :: _)) } //> traverse_1: [A, B](a: List[A])(f: A => Option[B])Option[List[B]] def sequenceViaTraverse[A](a: List[Option[A]]): Option[List[A]] = { traverse(a)(x => x) } //> sequenceViaTraverse: [A](a: List[Option[A]])Option[List[A]]
Option提升与for推导
因为提升非常常见,所以scala自带的for推导已经自带提升效果:
def map2[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推导:
def map2[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] = for { aa <- a bb <- b } yield f(aa, bb)
yield
关键字可以使用<-
符号左边绑定的值,编译器会把这些绑定操作的语法糖
转换为map调用。
Either数据类型
Either
和Option
一样可以用来替换异常或失败,区别是Option
只能用None
表示没有值
。不能传递更多关于产生错误原因的信息。Either
可以带有更多关于失败的详细信息:
sealed trait Either[+E, +A] case class Left[+E](get: E) extends Either[E, Nothing] case class Right[+A](get: A) extends Either[Nothing, A]
父类Either
的泛型E
代表错误信息的类型,A
代表有结果的结果类型。
两个子类Left
和Right
分别代表错误和正确。
// 求平均值,错误时返回的是相关错误信息的字符串: def mean(xs: IndexedSeq[Double]): Either[String, Double] = if (xs.isEmpty) Left("mean of empty list!") else Right(xs.sum / xs.length) // 还可以把堆栈调用信息放进去 def safeDiv(x: Int, y: Int): Either[Exception, Int] = try Right(x / y) catch { case e: Exception => Left(e) }
有时为了方便,当结果有两种类型的数据又不值得特地定义一个新的类的时候,
也常常用Either
应付一下。
把之前的Option类型的Try
函数改为用Either
类型:
def Try[A](a: => A): Either[Exception, A] = try Right(a) catch { case e: Exception => Left(e) }
常用函数的实现:
sealed trait Either[+E, +A] { def map[B](f: A => B): Either[E, B] = this match { case Right(a) => Right(f(a)) case Left(e) => Left(e) } // 对Right进行映射时,必须把Left的类型提升为父类型以满足`E+`的型变 def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = this match { case Right(a) => f(a) case Left(e) => Left(e) } // 对Right进行映射时,必须把Left的类型提升为父类型以满足`E+`的型变 def orElse[EE >: E, AA >: A](b: => Either[EE, AA]): Either[EE, AA] = this match { case Right(a) => Right(a) case Left(_) => b } def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] = for { a <- this; b1 <- b } yield f(a, b1) }
有了这些定义Either
也可以使用for推导:
def parseInsuranceRateQuote(age: String, numberOfSpeedingTickets: String): Either[Exception, Double] = { for { a <- Try(age.toInt) tickets <- Try(numberOfSpeedingTickets.toInt) } yield insuranceRateQuote(a, tickets) }
通过Either
还可以效验数据,比如检测构造函数是否有效:
sealed class Name(val value: String) sealed class Age (val value: Int) case class Person(name: Name, age: Age) def mkName(name: String): Either[String, Name] = if (name == "" || name == null) Left("Name is Empty.") else Right(new Name(name)) def mkAge(age: Int): Either[String, Age] = if (age < 0) Left("Age is out of range.") else Right(new Age(age)) def mkPerson(name: String, age: Int): Either[String, Person] = mkName(name).map2(mkAge(age))(Person(_, _))