Scala 高階函數(high-order function)剖析

Scala 是一種函數式編程語言,也就是說每個函數都是一個值。Scala 有很簡潔的語法用於定義匿名函數、curry 化函數(curried function)、應用函數、偏函數以及嵌套函數等。函數式編程由數學函數演變得來,包含大量諸如串聯與並聯、組合與分離、協變與逆變等一系列概念。本文將就如何實現高階函數進行闡述,其餘部份內容不進行深究。 shell

類型參數化協變與逆協變

類型參數化(type parameterization)由類型協變性(Type variance)補充構成,類型協變性的機制則是在類型系統中經過指定諸如協變性(convariant)和逆協變性(contravariant)進行約束。子類型的關係給協變帶來了問題——在類型參數化中,子類型之間是什麼關係?在Scala中,你能夠經過class和trait實現參數化。當在class和trait中使用類型參數時,你可使用 + 號實現協變。類型協變(Convriance)容許對其父類的協變位置中(如返回值)進行重載和使用狹義類型。即 編程

Scala 容許經過「+/-」定義類型參數的協變性, 安全

  1. 用「+」放在類型參數前表示構造子對於該參數是協變的;
  2. 「-」則表示逆協變;
  3. 沒有任何符號則表示非協變。
協變至關於Java的泛型T,反之則不成立,逆協變不是類型的強制轉換(cast)。由於,這裏還涉及到可變(variant)與不可變(invariant)的概念。

可變對象應該保持不變

可變(mutable)對象應該保持不變(invariant),爲何?在Scala中,要達到Scalable,就要實現相應的轉換能力。一個類型參數應該是不變(invariant),不論它是協變的仍是逆協變的。全部Scala可變集合類都是不變的。咱們用一個例子來講明爲何可變對象應該是不變的。咱們用反證法來論證,如今,你可使用collection.immutable.List以及對應的可變collection.mutable.ListBuffer。由於ListBuffer是可變的,它被聲明是不變的: 閉包

final class ListBuffer[A] ...{ ... }

注意,若是聲明瞭一個不可變類型(invarint type),你須要去掉-和+標記符號,由於你不能再爲ListBuffer指定其它類型。所以下面會發生編譯錯誤: app

scala> val mxs: ListBuffer[String] = ListBuffer("pants")
mxs: scala.collection.mutable.ListBuffer[String] =
ListBuffer(pants)
scala> val everything: ListBuffer[Any] = mxs
<console>:6: error: type mismatch;
found : scala.collection.mutable.ListBuffer[String]
required: scala.collection.mutable.ListBuffer[Any]
val everything: ListBuffer[Any] = mxs

儘管String是scala.Any的子類型,Scala不會將mxs指向到everything,爲了理解爲何,咱們假設ListBuffer是可變的,而且下列代碼不會發生任何編譯錯誤: 編程語言

scala> val mxs: ListBuffer[String] = ListBuffer("pants")
mxs: scala.collection.mutable.ListBuffer[String] =
ListBuffer(pants)
scala> val everything: ListBuffer[Any] = mxs
scala> everything += 1
res4: everything.type = ListBuffer(1, pants)

你發現到問題了沒有?由於everything是Any類型的,你不能存儲任何整型值到一個字符型的集合,這簡直在等待災難的發生。爲了不這類型問題的發生,把一個可變對象(mutable objects)保持不變(invariant)是最好不過的辦法。若是是集合中不可變對象的話會發生什麼?置於不可變對象的協變不會發生任何問題。若是你把ListBuffer改成List,你會直接得到一個指向List[Any]的List[String]實例而不發生任何問題。 函數式編程

scala> val xs: List[String] = List("pants")
xs: List[String] = List(pants)
scala> val everything: List[Any] = xs
everything: List[Any] = List(pants)

這樣指向安全的緣由是List是不可變的,你能夠添加1 到 xs列表中,而且他會返回一個新的Any類型的List: 函數

scala> 1 :: xs
res5: List[Any] = List(1, pants)

再說一次,上述方法是正確的,由於con(::)方法老是返回一個新的List,而它的類型取決於List元素的類型。這裏惟一能夠同時存儲一個整形值類型和一個引用值類型的類型是scala.Any。請記住,這是可變/不可變對象協變一項重要屬性。 測試

實際上,函數類型和函數值只不過是相應的類及其實例的語法糖衣。函數類型S => T 等價於參數化類型scala.Function1[S, T],這個類型定義在Scala 標準類庫中: ui

trait Function1[-P, +R] { ... }

參數超過一個的函數也可相似地定義,通常而言,n-元函數類型:(T1, T2, …, Tn) => T被解釋爲Functionn[T1, T2, …, Tn, T]。也就是說,函數就是擁有apply 方法的對象。例如,匿名函數「+1」:x: int => x+1,就是以下函數Function1 的實例:

new Function1[int, int] {
  def apply(x: int): int = x + 1
}

Scala使用減號(-)表示逆協變,加號(+)表示協變。在Function1中,P是逆協變的,R是協變的。在Scala中,函數包含值和類型。例如,Function1表示任何接收一個參數的函數,問題是爲何Function1對參數逆協變而對返回類型協變。 

在回答問題以前,咱們用反證法進行論證——對參數協變、對返回類型逆協變會發生什麼?假若有這樣一個協變參數:
val addOne: Function1[Any, Int] = { x: Int => x + 1 }

由於Int是scala.Any的子類,協變參數應該容許上面的代碼編譯。這段代碼的漏洞是你可使用任意的類型參數來調用addOne,只要參數是Any的子類。這樣作會引發一系列問題,由於該函數只接收Int。Scala做爲一門類型安全的(type-safe)語言,不容許你這樣作。另一個惟一可能就是你須要將參數類型聲明是不可變的(invariant),可是這樣會使得Function1不易擴展。建立一個類型安全的函數的惟一可行方案就是逆協變參數類型。你不能使用一個逆協變的返回類型,考慮以下代碼:

val asString: Int => Int = { x: Int => (x.toString: Any) }

這段代碼是無效的,由於Any是Int的超類,逆協變就是容許你從狹義類型到達廣義類型。那下面這個是否正確:

asString(10) + 20

代碼最後把20加進一個字符串值,這明顯有問題。在處理參數類型和返回類型時,Scala的強類型系統會停止這類錯誤的發生。要實現一個靈活的、類型安全的Function1特性,惟一可能實現的的方法就是 參數類型的逆協變和返回類型的協變。 

除了顧及參數類型和返回值以外,還要考慮類型子類化(subtyping)的邊界問題,即 B <: A下界和B >: A上界的約束問題。 因篇幅如今,不做贅言。 

考慮以上問題後,下面講述高階函數如何實現。

高階函數

函數做爲參數或做爲返回值的函數稱爲 高階函數。在Scala的immutable.List.的方法中存在大量的高階函數,咱們看看其中一個map方法

class List[+A] ... {
  def map[B](f: A => B) : List[B]
}

在編程語言中,除了值傳遞(call-by-value)、引用傳遞(call-by-value),還包括名傳遞(call-by-name)和需求傳遞(call-by-need)。上述代碼中,f: A => B這種函數做爲參數進行傳遞的就是名傳遞。因使用場景不一樣,名傳遞能夠是lambda匿名函數和詞法閉包的。像map這種高級函數能夠經過for-comrehension或者遞歸形式實現。

def map[A,B](xs:List[A],f:A=>B):List[B] = {
  xs match{
    case List() => Nil
    case head::tail=>f(head)::map(tail,f)
  }
}

上述map代碼中,經過模式匹配和 :: 組合實現一個類型參數傳遞的高階函數。其中f表示函數參數化處理操做。固然,你也能夠用尾遞歸實現並進行柯里化(currying)轉換。

添加一個測試例子:

@Test def testHighOrderFunction(){

  val xs = List(1, 2, 3)
  // 匿名函數做爲參數進行傳遞
  logger.info(s"${xs map ((x: Int) => x + 1)}")
  // 匿名函數只有一個參數時,略去參數外圍
  logger.info(s"${xs map (x => x + 1)}")
  // pointfree-style寫法,佔位符代替
  logger.info(s"${xs map (_ + 1)}")
  // 只傳遞函數名稱,函數已經實現包裝
  def addOne(x: Int) = x + 1
  logger.info(s"${xs map addOne}")

  /**
   * 遞歸實現的高階函數
   * @param xs List
   * @param f 函數
   */
  def map[A, B](xs: List[A], f: A => B): List[B] = {
    xs match {
      case List() => Nil
      case head :: tail => f(head) :: map(tail, f)
    }
  }

  logger.info(s"${map(xs, addOne)}")
}
相關文章
相關標籤/搜索