[Scala基础]--Either介绍
原文链接:https://windor.gitbooks.io/beginners-guide-to-scala/content/chp7-the-either-type.html
类型 Either
上一章介绍了 Try,它用函数式风格来处理程序错误。 这一章我们介绍一个和 Try 相似的类型 - Either, 学习如何去使用它,什么时候去使用它,以及它有什么缺点。
不过首先得知道一件事情: 在写作这篇文章的时候,Either 有一些设计缺陷,很多人都在争论到底要不要使用它。 既然如此,为什么还要学习它呢? 因为,在理解 Try 这个错综复杂的类型之前,不是所有人都会在代码中使用 Try 风格的异常处理。 其次,Try 不能完全替代 Either,它只是 Either 用来处理异常的一个特殊用法。 Try 和 Either 互相补充,各自侧重于不同的使用场景。
因此,尽管 Either 有缺陷,在某些情况下,它依旧是非常合适的选择。
Either 语义
Either 也是一个容器类型,但不同于 Try、Option,它需要两个类型参数: Either[A, B]
要么包含一个类型为 A
的实例,要么包含一个类型为 B
的实例。 这和 Tuple2[A, B]
不一样, Tuple2[A, B]
是两者都要包含。
Either 只有两个子类型: Left、 Right, 如果 Either[A, B]
对象包含的是 A
的实例,那它就是 Left 实例,否则就是 Right 实例。
在语义上,Either 并没有指定哪个子类型代表错误,哪个代表成功, 毕竟,它是一种通用的类型,适用于可能会出现两种结果的场景。 而异常处理只不过是其一种常见的使用场景而已, 不过,按照约定,处理异常时,Left 代表出错的情况,Right 代表成功的情况。
创建 Either
创建 Either 实例非常容易,Left 和 Right 都是样例类。 要是想实现一个 “坚如磐石” 的互联网审查程序,可以直接这么做:
import scala.io.Source
import java.net.URL
def getContent(url: URL): Either[String, Source] =if(url.getHost.contains("google"))Left("Requested URL is blocked for the good of the people!")elseRight(Source.fromURL(url))
调用 getContent(new URL("http://danielwestheide.com"))
会得到一个封装有 scala.io.Source
实例的 Right, 传入 new URL("https://plus.google.com")
会得到一个含有 String
的 Left。
Either 用法
Either 基本的使用方法和 Option、Try 一样: 调用 isLeft
(或 isRight
)方法询问一个 Either,判断它是 Left 值,还是 Right 值。 可以使用模式匹配,这是最方便也是最为熟悉的一种方法:
getContent(new URL("http://google.com")) match {case Left(msg) => println(msg)case Right(source) => source.getLines.foreach(println)
}
立场
你不能,至少不能直接像 Option、Try 那样把 Either 当作一个集合来使用, 因为 Either 是 无偏(unbiased)的。
Try 偏向 Success: map
、 flatMap
以及其他一些方法都假设 Try 对象是一个 Success 实例, 如果是 Failure,那这些方法不做任何事情,直接将这个 Failure 返回。
但 Either 不做任何假设,这意味着首先你要选择一个立场,假设它是 Left 还是 Right, 然后在这个假设的前提下拿它去做你想做的事情。 调用 left
或 right
方法,就能得到 Either 的 LeftProjection
或 RightProjection
实例, 这就是 Either 的 立场(Projection) ,它们是对 Either 的一个左偏向的或右偏向的封装。
映射
一旦有了 Projection,就可以调用 map
:
val content: Either[String, Iterator[String]] =getContent(new URL("http://danielwestheide.com")).right.map(_.getLines())
// content is a Right containing the lines from the Source returned by getContent
val moreContent: Either[String, Iterator[String]] =getContent(new URL("http://google.com")).right.map(_.getLines)
// moreContent is a Left, as already returned by getContent// content: Either[String,Iterator[String]] = Right(non-empty iterator)
// moreContent: Either[String,Iterator[String]] = Left(Requested URL is blocked for the good of the people!)
这个例子中,无论 Either[String, Source]
是 Left 还是 Right, 它都会被映射到 Either[String, Iterator[String]]
。 如果,它是一个 Right 值,这个值就会被 _.getLines()
转换; 如果,它是一个 Left 值,就直接返回这个值,什么都不会改变。
LeftProjection也是类似的:
val content: Either[Iterator[String], Source] =getContent(new URL("http://danielwestheide.com")).left.map(Iterator(_))
// content is the Right containing a Source, as already returned by getContent
val moreContent: Either[Iterator[String], Source] =getContent(new URL("http://google.com")).left.map(Iterator(_))
// moreContent is a Left containing the msg returned by getContent in an Iterator// content: Either[Iterator[String],scala.io.Source] = Right(non-empty iterator)
// moreContent: Either[Iterator[String],scala.io.Source] = Left(non-empty iterator)
现在,如果 Either 是个 Left 值,里面的值会被转换;如果是 Right 值,就维持原样。 两种情况下,返回类型都是 Either[Iterator[String, Source]
。
请注意, map 方法是定义在 Projection 上的,而不是 Either, 但其返回类型是 Either,而不是 Projection。
可以看到,Either 和其他你知道的容器类型之所以不一样,就是因为它的无偏性。 接下来你会发现,在特定情况下,这会产生更多的麻烦。 而且,如果你想在一个 Either 上多次调用 map 、 flatMap 这样的方法, 你总需要做 Projection,去选择一个立场。
Flat Mapping
Projection 也支持 flat mapping,避免了嵌套使用 map 所造成的令人费解的类型结构。
假设我们想计算两篇文章的平均行数,下面的代码可以解决这个 “富有挑战性” 的问题:
val part5 = new URL("http://t.co/UR1aalX4")
val part6 = new URL("http://t.co/6wlKwTmu")
val content = getContent(part5).right.map(a =>getContent(part6).right.map(b =>(a.getLines().size + b.getLines().size) / 2))
// => content: Product with Serializable with scala.util.Either[String,Product with Serializable with scala.util.Either[String,Int]] = Right(Right(537))
运行上面的代码,会得到什么? 会得到一个类型为 Either[String, Either[String, Int]]
的玩意儿。 当然,你可以调用 joinRight
方法来使得这个结果 扁平化(flatten) 。
不过我们可以直接避免这种嵌套结构的产生, 如果在最外层的 RightProjection 上调用 flatMap
函数,而不是 map
, 得到的结果会更好看些,因为里层 Either 的值被解包了:
val content = getContent(part5).right.flatMap(a =>getContent(part6).right.map(b =>(a.getLines().size + b.getLines().size) / 2))
// => content: scala.util.Either[String,Int] = Right(537)
现在, content
值类型变成了 Either[String, Int]
,处理它相对来说就很容易了。
for 语句
说到 for 语句,想必现在,你应该已经爱上它在不同类型上的一致性表现了。 在 for 语句中,也能够使用 Either
的 Projection,但遗憾的是,这样做需要一些丑陋的变通。
假设用 for 语句重写上面的例子:
def averageLineCount(url1: URL, url2: URL): Either[String, Int] =for {source1 <- getContent(url1).rightsource2 <- getContent(url2).right} yield (source1.getLines().size + source2.getLines().size) / 2
这个代码还不是太坏,毕竟只需要额外调用 left
、 right
。
但是你不觉得 yield 语句太长了吗?现在,我就把它移到值定义块中:
def averageLineCountWontCompile(url1: URL, url2: URL): Either[String, Int] =for {source1 <- getContent(url1).rightsource2 <- getContent(url2).rightlines1 = source1.getLines().sizelines2 = source2.getLines().size} yield (lines1 + lines2) / 2
试着去编译它,然后你会发现无法编译!如果我们把 for 语法糖去掉,原因可能会清晰些。 展开上面的代码得到:
def averageLineCountDesugaredWontCompile(url1: URL, url2: URL): Either[String, Int] =getContent(url1).right.flatMap { source1 =>getContent(url2).right.map { source2 =>val lines1 = source1.getLines().sizeval lines2 = source2.getLines().size(lines1, lines2)}.map { case (x, y) => x + y / 2 }}
问题在于,在 for 语句中追加新的值定义会在前一个 map
调用上自动引入另一个 map
调用, 前一个 map
调用返回的是 Either 类型,不是 RightProjection 类型, 而 Scala 并没有在 Either 上定义 map
函数,因此编译时会出错。
这就是 Either 丑陋的一面。要解决这个例子中的问题,可以不添加新的值定义。 但有些情况,就必须得添加,这时候可以将值封装成 Either 来解决这个问题:
def averageLineCount(url1: URL, url2: URL): Either[String, Int] =for {source1 <- getContent(url1).rightsource2 <- getContent(url2).rightlines1 <- Right(source1.getLines().size).rightlines2 <- Right(source2.getLines().size).right} yield (lines1 + lines2) / 2
认识到这些设计缺陷是非常重要的,这不会影响 Either 的可用性,但如果不知道发生了什么,它会让你感到非常头痛。
其他方法
Projection 还有其他有用的方法:
可以在 Either 的某个 Projection 上调用
toOption
方法,将其转换成 Option。假如,你有一个类型为
Either[A, B]
的实例e
,e.right.toOption
会返回一个Option[B]
。 如果e
是一个 Right 值,那这个Option[B]
会是 Some 类型, 如果e
是一个 Left 值,那Option[B]
就会是None
。 调用e.left.toOption
也会有相应的结果。- 还可以用
toSeq
方法将 Either 转换为序列。
Fold 函数
如果想变换一个 Either(不论它是 Left 值还是 right 值),可以使用定义在 Either 上的 fold
方法。 这个方法接受两个返回相同类型的变换函数, 当这个 Either 是 Left 值时,第一个函数会被调用;否则,第二个函数会被调用。
为了说明这一点,我们用 fold
重写之前的一个例子:
val content: Iterator[String] =getContent(new URL("http://danielwestheide.com")).fold(Iterator(_), _.getLines())
val moreContent: Iterator[String] =getContent(new URL("http://google.com")).fold(Iterator(_), _.getLines())
这个示例中,我们把 Either[String, String]
变换成了 Iterator[String]
。 当然,你也可以在变换函数里返回一个新的 Either,或者是只执行副作用。 fold
是一个可以用来替代模式匹配的好方法。
何时使用 Either
知道了 Either 的用法和应该注意的事项,我们来看看一些特殊的用例。
错误处理
可以用 Either 来处理异常,就像 Try 一样。 不过 Either 有一个优势:可以使用更为具体的错误类型,而 Try 只能用 Throwable
。 (这表明 Either 在处理自定义的错误时是个不错的选择) 不过,需要实现一个方法,将这个功能委托给 scala.util.control
包中的 Exception
对象:
import scala.util.control.Exception.catching
def handling[Ex <: Throwable, T](exType: Class[Ex])(block: => T): Either[Ex, T] =catching(exType).either(block).asInstanceOf[Either[Ex, T]]
这么做的原因是,虽然 scala.util.Exception
提供的方法允许你捕获某些类型的异常, 但编译期产生的类型总是 Throwable
,因此需要使用 asInstanceOf
方法强制转换。
有了这个方法,就可以把期望要处理的异常类型,放在 Either 里了:
import java.net.MalformedURLException
def parseURL(url: String): Either[MalformedURLException, URL] =handling(classOf[MalformedURLException])(new URL(url))
handling
的第二个参数 block
中可能还会有其他产生错误的情形, 而且并不是所有情形都会抛出异常。 这种情况下,没必要为了捕获异常而人为抛出异常,相反,只需定义你自己的错误类型,最好是样例类, 并在错误情况发生时返回一个封装了这个类型实例的 Left。
下面是一个例子:
case class Customer(age: Int)
class Cigarettes
case class UnderAgeFailure(age: Int, required: Int)
def buyCigarettes(customer: Customer): Either[UnderAgeFailure, Cigarettes] =if (customer.age < 16) Left(UnderAgeFailure(customer.age, 16))else Right(new Cigarettes)
应该避免使用 Either 来封装意料之外的异常, 使用 Try 来做这种事情会更好,至少它没有 Either 这样那样的缺陷。
处理集合
有些时候,当按顺序依次处理一个集合时,里面的某个元素产生了意料之外的结果, 但是这时程序不应该直接引发异常,因为这样会使得剩下的元素无法处理。 Either 也非常适用于这种情况。
假设,在我们 “行业标准般的” Web 审查系统里,使用了某种黑名单:
type Citizen = String
case class BlackListedResource(url: URL, visitors: Set[Citizen])val blacklist = List(BlackListedResource(new URL("https://google.com"), Set("John Doe", "Johanna Doe")),BlackListedResource(new URL("http://yahoo.com"), Set.empty),BlackListedResource(new URL("https://maps.google.com"), Set("John Doe")),BlackListedResource(new URL("http://plus.google.com"), Set.empty)
)
BlackListedResource
表示黑名单里的网站 URL,外加试图访问这个网址的公民集合。
现在我们想处理这个黑名单,为了标识 “有问题” 的公民,比如说那些试图访问被屏蔽网站的人。 同时,我们想确定可疑的 Web 网站:如果没有一个公民试图去访问黑名单里的某一个网站, 那么就必须假定目标对象因为一些我们不知道的原因绕过了筛选器,需要对此进行调查。
下面的代码展示了该如何处理黑名单的:
val checkedBlacklist: List[Either[URL, Set[Citizen]]] =blacklist.map(resource =>if (resource.visitors.isEmpty) Left(resource.url)else Right(resource.visitors))
我们创建了一个 Either 序列,其中 Left
实例代表可疑的 URL, Right
是问题市民的集合。 识别问题公民和可疑网站变得非常简单。
val suspiciousResources = checkedBlacklist.flatMap(_.left.toOption)
val problemCitizens = checkedBlacklist.flatMap(_.right.toOption).flatten.toSet
Either
非常适用于这种比异常处理更为普通的使用场景。
总结
目前为止,你应该已经学会了怎么使用 Either,认识到它的缺陷,以及知道该在什么时候用它。 鉴于 Either 的缺陷,使用不使用它,全都取决于你。 其实在实践中,你会注意到,有了 Try 之后,Either 不会出现那么多糟糕的使用情形。
不管怎样,分清楚它带来的利与弊总没有坏处。
[Scala基础]--Either介绍相关推荐
- scala函数式编程(二) scala基础语法介绍
上次我们介绍了函数式编程的好处,并使用scala写了一个小小的例子帮助大家理解,从这里开始我将真正开始介绍scala编程的一些内容. 这里会先重点介绍scala的一些语法.当然,这里是假设你有一些ja ...
- Scala学习(一)--Scala基础学习
Scala基础学习 摘要: 在篇主要内容:如何把Scala当做工业级的便携计算器使用,如何用Scala处理数字以及其他算术操作.在这个过程中,我们将介绍一系列重要的Scala概念和惯用法.同时你还将学 ...
- (数据科学学习手札45)Scala基础知识
一.简介 由于Spark主要是由Scala编写的,虽然Python和R也各自有对Spark的支撑包,但支持程度远不及Scala,所以要想更好的学习Spark,就必须熟练掌握Scala编程语言,Scal ...
- 23篇大数据系列(二)scala基础知识全集(史上最全,建议收藏)
作者简介: 蓝桥签约作者.大数据&Python领域优质创作者.管理多个大数据技术群,帮助大学生就业和初级程序员解决工作难题. 我的使命与愿景:持续稳定输出,赋能中国技术社区蓬勃发展! 大数据系 ...
- NLP汉语自然语言处理入门基础知识介绍
NLP汉语自然语言处理入门基础知识介绍 自然语言处理定义: 自然语言处理是一门计算机科学.人工智能以及语言学的交叉学科.虽然语言只是人工智能的一部分(人工智能还包括计算机视觉等),但它是非常独特的一部 ...
- 【 MATLAB 】逆离散余弦变换(idct)的基础知识介绍
基础知识介绍 逆离散余弦变换从离散余弦变换 (DCT) 系数中重建序列.idct 函数是 dct 函数的逆. The DCT has four standard variants. For a tra ...
- python基础知识点-Python入门基础知识点(基础语法介绍)
计算机基础知识介绍 计算机核心部件分为CPU,内存,硬盘,操作系统 1.CPU:中央处理器,相当于人大脑 2.内存:临时存储数据.现在通常分为 8g和16g(不能替代硬盘的原因:1,成本高 2,断电即 ...
- linux vim编辑文本是 m,linux基础命令介绍四:文本编辑 vim
本文介绍vim(版本7.4)的一般用法 vim是功能强大的文本编辑器,是vi的增强版. vim [options] [file ..] 使用vim编辑一个文件的最常用命令就是: vim file 其中 ...
- Socket基础API介绍
文章目录 1 Socket基础API介绍 1 Socket基础API介绍 我们先来看下使用Socket API建立简易TCP服务端和客户端的步骤: 用Socket API建立简易TCP服务端: 建立一 ...
- Scala基础教程(一):简介、环境安装
Scala基础语法 如果有很好的了解Java语言,那么将很容易学习Scala. Scala和Java间的最大语法的区别在于;行结束符是可选的.考虑Scala程序它可以被定义为通过调用彼此方法进行通信的 ...
最新文章
- vue 02-上计算属性、样式的操作,指令(含自定义,全局和局部)
- [译]使用JavaScript来操纵数据视图DataView新建视图的默认值
- dir在python中什么意思_python中dir是什么
- 使用redis实现异步消息队列
- spark 流式计算_流式传输大数据:Storm,Spark和Samza
- 51单片机dds信号发生器 扫频 c语言,基于AT89C51单片机和DDS器件实现频率特征测试仪的设计...
- WEB字体,多列布局和伸缩盒
- 如何找到字符串中的最长回文子串?
- fastjson List 转Json , Json 转List
- 离散数学常见面试问题总结,含答案
- 使用MD5加密的登陆demo
- python实验中遇到的问题及解决方法_Python中遇到的小问题及解决方法汇总
- neo4j图形数据库Java应用
- win10高危服务_简单教你Win10哪些服务项可禁用关闭,爱纯净官网
- 005 GO-高级数据类型(结构体和方法)
- application.yml图标不是绿色小叶子,文件格式不对,,没有Spring环境问题
- 小波变换的尺度函数和小波函数分析
- 力回馈方向盘测试软件,真假如何辨?力反馈方向盘深度剖析
- 交叉验证(CrossValidation)方法思想简介
- 计网-2-标准化工作及其相关组织