函數(shù)式編程

2018-02-24 15:48 更新

面向值(value-oriented )編程有很多優(yōu)勢(shì),特別是用在與函數(shù)式編程結(jié)構(gòu)相結(jié)合。這種風(fēng)格強(qiáng)調(diào)值的轉(zhuǎn)換(譯注:由一個(gè)不變的值生成另一個(gè)不變的值)而非狀態(tài)的改變,生成的代碼是指稱透明的(referentially transparent),提供了更強(qiáng)的不變型(invariants),因此容易實(shí)現(xiàn)。Case類(也被翻譯為樣本類),模式匹配,解構(gòu)綁定(destructuring bindings),類型推斷,輕量級(jí)的閉包和方法創(chuàng)建語(yǔ)法都是這一類的工具。

Case類模擬代數(shù)數(shù)據(jù)類型

Case類可實(shí)現(xiàn)代數(shù)數(shù)據(jù)類型(ADT)編碼:它們對(duì)大量的數(shù)據(jù)結(jié)構(gòu)進(jìn)行建模時(shí)有用,用強(qiáng)不變類型(invariants)提供了簡(jiǎn)潔的代碼。尤其在結(jié)合模式匹配情況下。模式匹配實(shí)現(xiàn)了全面解析提供更強(qiáng)大的靜態(tài)保護(hù)。 (譯注:ADTs是Algebraic Data Type代數(shù)數(shù)據(jù)類型的縮寫(xiě),關(guān)于這個(gè)概念見(jiàn)我的另一篇博客)

下面是用case類模擬代數(shù)數(shù)據(jù)類型的模式

 sealed trait Tree[T]
 case class Node[T](left: Tree[T], right: Tree[T]) extends Tree[T]
 case class Leaf[T](value: T) extends Tree[T]

類型 Tree[T] 有兩個(gè)構(gòu)造函器:Node和Leaf。定義類型為sealed(封閉類)允許編譯器進(jìn)行徹底的分析(這是針對(duì)模式匹配的,參考Programming in Scala)因?yàn)闃?gòu)造器將不能從外部源文件中添加。

與模式匹配一同,這個(gè)建模使得代碼簡(jiǎn)潔并且顯然是正確的(obviously correct)

 def findMin[T <: Ordered[T]](tree: Tree[T]) = tree match {
   case Node(left, right) => Seq(findMin(left), findMin(right)).min
   case Leaf(value) => value
 }

盡管一些遞歸結(jié)構(gòu),如樹(shù)的組成是典型的ADTs(代數(shù)數(shù)據(jù)類型)應(yīng)用,它們的用處領(lǐng)域更大。 disjoint,unions特別容易的用ADTs建模;這些頻繁發(fā)生在狀態(tài)機(jī)上(state machines)。

Options

Option類型是一個(gè)容器,空(None)或滿(Some(value))二選一。它提供了使用null的另一種安全選擇,應(yīng)該盡可能的替代null。它是一個(gè)集合(最多只有一個(gè)元素)并用集合操所修飾,盡量用Option。

 var username: Option[String] = None
 ...
 username = Some("foobar")

代替

 var username: String = null
 ...
 username = "foobar"

因?yàn)榍罢吒踩篛ption類型靜態(tài)地強(qiáng)制username必須對(duì)空(emptyness)做檢測(cè)。

對(duì)一個(gè)Option值做條件判斷應(yīng)該用foreach

 if (opt.isDefined)
   operate(opt.get)

上面的代碼應(yīng)該用下面的方式替代:

 opt foreach { value =>
   operate(value)}

風(fēng)格可能看起來(lái)有些古怪,但更安全,更簡(jiǎn)潔。如果兩種情況都有(Option的None或Some),用模式匹配

 opt match {
   case Some(value) => operate(value)
   case None => defaultAction()
 }

但如果缺少的是缺省值,用getOrElse方法:

 operate(opt getOrElse defaultValue)

不要過(guò)度使用Option: 如果有一個(gè)明確的缺省值——一個(gè)Null對(duì)象——直接用Null而不必用Option

Option還有一個(gè)方便的構(gòu)造器用于包裝空值(nullable value)

 Option(getClass.getResourceAsStream("foo"))

得到一個(gè) Option[InputStream] 假定空值(None)時(shí)getResourceAsStream會(huì)返回null。

模式匹配

模式匹配(x match { …) 在良好的Scala代碼中無(wú)處不在:用于合并條件執(zhí)行、解構(gòu)(destructuring) 、在構(gòu)造中造型。使用好模式匹配可以增加程序的明晰度和安全性。

使用模式匹配實(shí)現(xiàn)類型轉(zhuǎn)換:

 obj match {
   case str: String => ...
   case addr: SocketAddress => ...

模式匹配在和解構(gòu)(destructuring)聯(lián)合使用時(shí)效果最好(例如你要匹配case類);下面的寫(xiě)法

 animal match {
   case dog: Dog => "dog (%s)".format(dog.breed)
   case _ => animal.species
   }

應(yīng)該被替代為:

 animal match {
   case Dog(breed) => "dog (%s)".format(breed)
   case other => other.species
 }

寫(xiě)自定義的抽取器?(extractor)時(shí)必須有雙重構(gòu)造器(譯注:成對(duì)出現(xiàn)的apply方法與unapply方法),否則可能是不適合的。

當(dāng)默認(rèn)的方法更有意義時(shí),對(duì)條件執(zhí)行不要用模式匹配。集合庫(kù)的方法通常返回Options,避免:

 val x = list match {
   case head :: _ => head
   case Nil => default
 }

因?yàn)?/p>

 val x = list.headOption getOrElse default

更短并且更能表達(dá)目的。

偏函數(shù)

Scala提供了定義PartialFunction的語(yǔ)法簡(jiǎn)寫(xiě):

 val pf: PartialFunction[Int, String] = {
   case i if i%2 == 0 => "even"
 }

它們也可能和 orElse 組合:

 val tf: (Int => String) = pf orElse { case _ => "odd"}

 tf(1) == "odd"
 tf(2) == "even"

偏函數(shù)出現(xiàn)在很多場(chǎng)景,并以PartialFunction有效地編碼 ,例如 方法參數(shù):

 trait Publisher[T] {
   def subscribe(f: PartialFunction[T, Unit])
 }

 val publisher: Publisher[Int] = ..
 publisher.subscribe {
   case i if isPrime(i) => println("found prime", i)
   case i if i%2 == 0 => count += 2
   /* ignore the rest */
 }

或在返回一個(gè)Option的情況下:

 // Attempt to classify the the throwable for logging.
 type Classifier = Throwable => Option[java.util.logging.Level]

可以更好的用PartialFunction表達(dá)

 type Classifier = PartialFunction[Throwable, java.util.Logging.Level]

因?yàn)樗峁┝烁玫目山M合性:

 val classifier1: Classifier
 val classifier2: Classifier

 val classifier = classifier1 orElse classifier2 orElse { _ => java.util.Logging.Level.FINEST }

解構(gòu)綁定

解構(gòu)綁定與模式匹配有關(guān)。它們用了相同的機(jī)制,但解構(gòu)綁定可應(yīng)用在當(dāng)匹配只有一種選項(xiàng)的時(shí)候 (以免你接受異常的可能)。解構(gòu)綁定特別適用于元組(tuple)和樣本類(case class).

 val tuple = ('a', 1)
 val (char, digit) = tuple

 val tweet = Tweet("just tweeting", Time.now)
 val Tweet(text, timestamp) = tweet

惰性賦值

當(dāng)使用lazy修飾一個(gè)val成員時(shí),其賦值情況是在需要時(shí)才賦值的(by need),因?yàn)镾cala中成員與方法是等價(jià)的(除了private[this]成員)

 lazy val field = computation()

相當(dāng)于下面的簡(jiǎn)寫(xiě):

 var _theField = None
 def field = if (_theField.isDefined) _theField.get else {
   _theField = Some(computation())
   _theField.get
 }

也就是說(shuō),它在需要時(shí)計(jì)算結(jié)果并會(huì)記住結(jié)果,在要達(dá)到這種目的時(shí)使用lazy成員;但當(dāng)語(yǔ)意上需要惰性賦值時(shí)(by semantics),要避免使用惰性賦值,這種情況下,最好顯式賦值因?yàn)樗沟贸杀灸P褪敲鞔_的,并且副作用被嚴(yán)格的控制。

Lazy成員是線程安全的。

傳名調(diào)用

方法參數(shù)可以指定為傳名參數(shù) (by-name) 意味著參數(shù)不是綁定到一個(gè)值,而是一個(gè)可能需要反復(fù)進(jìn)行的計(jì)算。這一特性需要小心使用; 期待傳值(by-value)語(yǔ)法的調(diào)用者會(huì)感到驚訝。這一特性的動(dòng)機(jī)是構(gòu)造語(yǔ)法自然的 DSLs——使新的控制結(jié)構(gòu)可以看起來(lái)更像本地語(yǔ)言特征。

只在下面的控制結(jié)構(gòu)中使用傳名調(diào)用, 調(diào)用者明顯傳遞的是一段代碼塊(block)而非一個(gè)確定的計(jì)算結(jié)果。傳名參數(shù)必須放在參數(shù)列表的最后一位。當(dāng)使用傳名調(diào)用時(shí),確保方法名稱讓調(diào)用者明顯感知到方法參數(shù)是傳名參數(shù)。

當(dāng)你想要一個(gè)值被計(jì)算多次,特別是這個(gè)計(jì)算會(huì)引起副作用時(shí),使用顯式函數(shù):

 class SSLConnector(mkEngine: () => SSLEngine)

這樣意圖很明確,調(diào)用者不會(huì)感到驚奇。

flatMap

flatMap——結(jié)合了map 和 flatten —— 的使用要特別小心,它有著難以琢磨的威力和強(qiáng)大的實(shí)用性。類似它的兄弟 map,它也是經(jīng)常在非傳統(tǒng)的集合中使用的,例如 Future , Option。它的行為由它的(函數(shù))簽名揭示;對(duì)于一些容器 Container[A]

 flatMap[B](f: A => Container[B]): Container[B]

flatMap對(duì)集合中的每個(gè)元素調(diào)用了 函數(shù) f 產(chǎn)生一個(gè)新的集合,將它們?nèi)?flatten 后放入結(jié)果中。例如,獲取兩個(gè)字符的字符串的所有排列,相同的字符不能出現(xiàn)兩次

 val chars = 'a' to 'z'
 val perms = chars flatMap { a =>
   chars flatMap { b =>
     if (a != b) Seq("%c%c".format(a, b))
     else Seq()
   }
 }

等價(jià)于下面這段更簡(jiǎn)潔的 for-comprehension (基本就是針對(duì)上面的語(yǔ)法糖)

 val perms = for {
   a <- chars
   b <- chars
   if a != b
 } yield "%c%c".format(a, b)

flatMap在處理Options常常很有用—— 它將多個(gè)options鏈合并為一個(gè),

 val host: Option[String] = ..
 val port: Option[Int] = ..

 val addr: Option[InetSocketAddress] =
   host flatMap { h =>
     port map { p =>
       new InetSocketAddress(h, p)
     }
   }

也可以使用更簡(jiǎn)潔的for來(lái)實(shí)現(xiàn):

 val addr: Option[InetSocketAddress] = for {
   h <- host
   p <- port
 } yield new InetSocketAddress(h, p)

對(duì)flatMap在在Futures中的使用futures一節(jié)中有討論。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)