深入探索Scala的Option

程序员最深恶痛绝并力求避免的异常是NullPointerException,很不幸,我们往往又会忽略这个错误。不知是谁设计了Null这样的对象。我在文章《并非Null Object这么简单》中已经阐释了这个问题。然而不仅仅是空指针异常,当程序代码中出现各种错误时,我们的处理方式该如何呢?

现在,让我们再看看Scala语法层面的Option。Option对象并没有从根本上解决程序错误的问题,但只要使用得当,就能有效地将错误往程序的外层推,这实际上是消除副作用的惯常做法。正如Paul Chiusano等人的著作《Scala函数式编程》描述的那样:

对函数式程序员而言,程序的实现,应该有一个纯的内核和一层很薄的外围来处理副作用。

REA的Scala程序员Ken Scambler在YOW!大会上有一个很棒的演讲2 Year of Real World FP at REA。演讲中提到REA选择函数式编程的三个原因:

  • 模块化(Modularity)
  • 抽象(Abstraction)
  • 可组合性(Composability)

模块化的一个重要特征是设计没有副作用的纯函数,这样就不会影响调用该纯函数的上下文,也就是所谓的“引用透明(Reference Transparency)”概念,即针对任何函数都可以使用它的返回值来代替(substitution)。

Ken Scambler使用了一个解析字符串的例子来阐释这种纯函数特质。如下代码所示:

def parseLocation(str: String): Location = { 
  val parts = str.split(",") 
  val secondStr = parts(1) 
  val parts2 = secondStr.split(“ “) 
  Location( parts(0), parts2(0), parts(1).toInt)
}

这段代码可能存在如下错误:

  • 作为input的str可能为null
  • parts(0)和parts(1)可能导致索引越界
  • parts2(0)可能导致索引越界
  • parts(1)未必是整数,调用toInt可能导致类型转换异常

仅仅从函数的定义来看,我们其实看不到这些潜在的负面影响。那么,想像一下当这样的方法被系统中许多地方直接或间接调用,可能会造成什么样的灾难?!假设这样的代码被放到安全要求极高的系统中,你是否会感到不寒而栗?——程序员,应该在道义上肩负为自己所写每行代码承担自己的责任,这是基本的职业素养。我所谓的承担责任,并不是事后追究,而是在每次写完代码后都要再三推敲,力求每行代码都是干净利落,没有歧义,没有潜在的错误。

然而,针对以上代码,要怎样才能保证程序调用的健壮性呢?就是要对可能出现的错误(空对象,索引越界,类型转换异常)进行判断。这就需要在parseLocation函数体中加入一堆if语句,短短的六行代码可能会膨胀一倍,而分支语句也会让程序的逻辑变得凌乱,正常逻辑与异常逻辑可能会像麻花一样扭在一起。当然,我们可以运用防御式编程,将可能的错误防御在正常逻辑代码之前,但它带来的阅读体验却会非常糟糕。

即使针对这些错误进行判断,仍然无法解决的一个问题是当对象真的出现错误时,函数实现究竟该如何处理?多数语言不支持多返回值(乃至不支持类似Scala的Pair),经典的解决办法就是抛出异常,可惜,异常却存在副作用。许多程序员更习惯性的返回了null。这是最要命的做法,就好像是慕容复的“以彼之道,还施彼身”,典型地损人利己!

引入Option,会让代码在保证健壮性的同时还保证了简洁性,例如:

def parseLocation(str: String): Option[Location] = {
  val parts = str.split(",")
  for {
    locality <- parts.optGet(0)
    theRestStr <- parts.optGet(1)

    theRest = theRestStr.split(" ")
    subdivision <- theRest.optGet(0)
    postcodeStr <- theRest.optGet(1)
    postcode <- postcodeStr.optToInt
} yield Location(locality, subdivision, postcode)}

本质上,Option是一个Monad;简单来说,是一个定义了flatMap与map的容器,故而能够支持Scala中可读性更佳的for comprehension。如上代码简单明了,你甚至可以忽略当Option为None的情形,只考虑正常的字符串解析逻辑,它自然地隐含了None的语义,因为在代码中通过optGet与optToInt返回的值(为Option类型),只要其中一个为None,整个函数就将即刻返回一个None对象,而非一个包裹了Location的Some对象。这样既避免了使用分支语句,还能使得函数没有任何副作用,规避了抛出异常的逻辑。

如上的改进仍然存在一个问题,那就是缺乏对输入的str进行判断。一个好的API设计者或者函数实现者,要怀着“性本恶”的悲观主义道德观,对任何输入持怀疑态度,且不惮于怀疑调用者的恶意。对于输入的这个str,我们仍然要避免使用条件判断的方式,因而可以修改函数的接口为:

def parseLocation(str: Option[String]): Option[Location] = ???

如此,我们可以将对str的解析逻辑也挪动到for comprehension中:

def parseLocation(str: Option[String]): Option[Location] = {
  for {
    val parts = str.split(",")
    locality <- parts.optGet(0)
    theRestStr <- parts.optGet(1)

    theRest = theRestStr.split(" ")
    subdivision <- theRest.optGet(0)
    postcodeStr <- theRest.optGet(1)
    postcode <- postcodeStr.optToInt
} yield Location(locality, subdivision, postcode)}

使用Option的唯一问题是:你虽然指定了Option这样的游戏规则,但其他API的设计者却未必按照你设计的规则出牌。这也是如上代码中optGet之类函数的由来。即使是Scala的内置库,如String的split函数,返回的也并非一个Option,而是一个普通的数组。当我们给一个错误的下标值去访问数组时,有可能会抛出ArrayIndexOutOfBoundsException异常。

Scala提供的解决方案是隐式转换(implicit conversion)。split()
函数返回的类型为Array[String],该类型自身是没有optGet()
函数的。但是我们可以为Array[String]定义隐式转换:

implicit class ArrayWrapper(array: Array[String]) {
  def optGet(index:Int): Option[String] = 
    if (array.length > index) Some(array(index)) else None
}

optToInt方法可以如法炮制。

惯常说来,当我们在使用Option时,习惯于利用模式匹配(pattern match)以运用“分而治之”的思想来编写代码。然而,多数时候我们应该使用定义在Option中的函数,这些函数可以让代码变得更简单。例如使用flatMap、map、isDefined、isEmpty、forAll、exists、orElse、getOrElse等函数。Tony Morris整理的scala.Option Cheat Sheet总结了这些函数的用法,可供参考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,640评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,254评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,011评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,755评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,774评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,610评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,352评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,257评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,717评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,894评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,021评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,735评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,354评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,936评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,054评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,224评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,974评论 2 355

推荐阅读更多精彩内容