您好,歡迎來(lái)電子發(fā)燒友網(wǎng)! ,新用戶?[免費(fèi)注冊(cè)]

您的位置:電子發(fā)燒友網(wǎng)>源碼下載>匯編編程>

函數(shù)式編程的基本特性

大?。?/span>0.5 MB 人氣: 2017-10-10 需要積分:1
 本文簡(jiǎn)單介紹了一下函數(shù)式編程的各種基本特性,希望能夠?qū)τ跍?zhǔn)備使用函數(shù)式編程的人起到一定入門(mén)作用。
  
  函數(shù)式編程,一個(gè)一直以來(lái)都酷,很酷,非??岬拿~。雖然誕生很早也炒了很多年但是一直都沒(méi)有造成很大的水花,不過(guò)近幾年來(lái)隨著多核,分布式,大數(shù)據(jù)的發(fā)展,函數(shù)式編程已經(jīng)廣泛投入到了實(shí)戰(zhàn)中。
  然而現(xiàn)實(shí)中還是有不少人不太了解這種編程范式,覺(jué)得這僅僅是一個(gè)逼格較高的名詞。我們這里就來(lái)簡(jiǎn)單介紹一下這個(gè)舉手投足都充滿酷勁的小東西。
  本文之后的代碼主要以 Java 和 Scala 為主,前者說(shuō)明如何在非函數(shù)式語(yǔ)言中實(shí)現(xiàn)函數(shù)式風(fēng)格,后者說(shuō)明在函數(shù)式語(yǔ)言中是如何做的。代碼比較簡(jiǎn)單,無(wú)論你是否懂這兩門(mén)語(yǔ)言,相信都能很容易看懂。此外由于函數(shù)式編程這幾個(gè)詞太長(zhǎng)了,以下都以 FP 進(jìn)行簡(jiǎn)寫(xiě)。
  特性
  函數(shù)是一等公民
  所謂的函數(shù)是一等公民指的是在 FP 中,函數(shù)可以作為直接作為變量的值。
  例
  Scala
  val add = (x: Int, y: Int) =》 x + y
  add(1, 2)
  以上我們定義了一個(gè)負(fù)責(zé)將兩個(gè)整型相加的匿名函數(shù)并賦值給變量 add,并且直接將這個(gè)變量當(dāng)前函數(shù)進(jìn)行調(diào)用,這在大部分面向?qū)ο蟮恼Z(yǔ)言中你都是無(wú)法直接這樣做的。
  Java
  interface Adder {
  int add(int x, int y);
  }
  Adder adder = (x, y) -》 x + y;
  adder.add(1, 2);
  由于 Java 并不是函數(shù)式語(yǔ)言,所以無(wú)法直接將函數(shù)賦值給變量,因此以上例子中我們使用 SAM 轉(zhuǎn)換來(lái)實(shí)現(xiàn)近似功能。
  閉包
  閉包是一種帶有自由變量的代碼塊,其最根本的功能就是能夠擴(kuò)大局部變量的生命周期。閉包相信很多人都很熟悉,在 Java 中閉包無(wú)處不在,是一種很好用但是一不注意就會(huì)掉坑里的特性。
  例
  Scala
  var factor = 10
  factor = factor * 10
  val multiplier = (x: Int) =》 x * factor
  以上例子中函數(shù)體使用了兩個(gè)參數(shù),其中 x 只是很普通的函數(shù)參數(shù),而 factor 則是函數(shù)體外定義的一個(gè)局部變量,且該變量可以任意進(jìn)行修改,所以對(duì) factor 的引用使該函數(shù)變成了一個(gè)閉包。
  Java
  int factor = 10;
  // factor = factor * 10;
  Multiplier multiplier = (x) -》 x * factor;
  在 Java 中匿名函數(shù)只能引用外部的 final 變量,Java 8 雖然可以省略 final 關(guān)鍵字,但是實(shí)際還是沒(méi)有任何變化,所以第二句語(yǔ)句必須注釋掉。這也就是說(shuō)在 Java 中實(shí)際是無(wú)法使用自由變量的,因此 Java 是否有真正的閉包一直都是一個(gè)爭(zhēng)論點(diǎn),這里就不多牽扯了。
  惰性求值 Lazy Evaluation
  一般而言成員變量在實(shí)例創(chuàng)建時(shí)就會(huì)被初始化,而惰性求值可以將初始化的過(guò)程延遲到變量的第一次使用,對(duì)于成員變量的值需要經(jīng)過(guò)大量計(jì)算的類來(lái)說(shuō)可以有效加快實(shí)例的創(chuàng)建過(guò)程。
  例
  Scala
  lazy val lazyField = {
  var sum = 0
  for (i 《- 1 to 100) {
  sum += i
  }
  sum
  }
  在 Scala 中是通過(guò)關(guān)鍵字 lazy 來(lái)聲明惰性求值的。在以上例子中定義了一個(gè)從 1 加到 100 的惰性變量,在第一次訪問(wèn)該變量時(shí)這個(gè)計(jì)算過(guò)程才會(huì)被執(zhí)行。
  Java
  Supplier《Integer》 lazyField = () -》 {
  int sum = 0;
  for (int i = 1; i 《= 100; i++) {
  sum += i;
  }
  return sum;
  };
  Java 雖然在語(yǔ)言層面沒(méi)有提供該功能,但是可以通過(guò) Java 8 提供的 Supplier 接口來(lái)實(shí)現(xiàn)同樣的功能。
  尾遞歸 Tail Recursion
  遞歸大家都知道,就是函數(shù)自己調(diào)用自己。
  例
  定義一個(gè)遞歸函數(shù)
  def addOne(i: Int) {
  if (i 》 3) return
  println(s“before $i”)
  addOne(i + 1)
  println(s“after $i”)
  }
  調(diào)用以上函數(shù)并傳入?yún)?shù) 3 會(huì)打印如下語(yǔ)句
  before 1
  before 2
  before 3
  after 3
  after 2
  after 1
  這就是遞歸的基本形式。在每次遞歸調(diào)用時(shí)程序都必須保存當(dāng)前的方法調(diào)用棧,即調(diào)用 addOne(2) 時(shí)程序必須記住之前是如何調(diào)用 addOne(1) 的,這樣它才能在執(zhí)行完 addOne(2) 后返回到 addOne(1) 的下一條語(yǔ)句并打印 after 1。因此在 Java 等語(yǔ)言中遞歸一來(lái)影響效率,二來(lái)消耗內(nèi)存,調(diào)用次數(shù)過(guò)多時(shí)會(huì)引起方法棧溢出。
  而尾遞歸指的就是只在函數(shù)的最后一個(gè)語(yǔ)句調(diào)用遞歸。這樣的好處是可以使用很多 FP 語(yǔ)言都支持的尾遞歸優(yōu)化或者叫尾遞歸消除,即遞歸調(diào)用時(shí)直接將函數(shù)的調(diào)用者傳入到下一個(gè)遞歸函數(shù)中,并將當(dāng)前函數(shù)彈出棧中,在最后一次遞歸調(diào)用完畢后直接返回傳入的調(diào)用者處而不是返回上一次遞歸的調(diào)用處。
  用簡(jiǎn)單的示意圖即是由原來(lái)的
  line xxx, call addOne -》 addOne(1) -》 addOne(2) -》 addOne(3) -》 addOne(2) -》 addOne(1) -》 line xxx
  優(yōu)化為
  line xxx, call addOne -》 addOne(1) -》 addOne(2) -》 addOne(3) -》 line xxx
  純函數(shù) Pure Function
  純函數(shù)并不是 FP 的特性,而是 FP 中一些特性的集合。所謂的純函數(shù)簡(jiǎn)單來(lái)講就是函數(shù)不能有副作用,保證引用透明。即函數(shù)本身不會(huì)修改參數(shù)的值也不會(huì)修改函數(shù)外的變量,無(wú)論執(zhí)行多少次,同樣的輸入都會(huì)有同樣的輸出。
  例
  定義三個(gè)函數(shù)
  def add(x: Int, y: Int) = x + y
  def clear(list: mutable.MutableList): Unit = {
  list.clear()
  }
  def random() = Random.nextInt()
  以上代碼中定義了三個(gè)函數(shù),其中 add() 符合純函數(shù)的定義;clear() 會(huì)清除傳入的 List 的所有元素,所以不是純函數(shù);random() 無(wú)法保證每次調(diào)用都產(chǎn)生同樣的輸入,所以也不是純函數(shù)。
  高階函數(shù) High-Order Function
  高階函數(shù)指一個(gè)函數(shù)的參數(shù)是另一個(gè)函數(shù),或者一個(gè)函數(shù)的返回值是另一個(gè)函數(shù)。
  例
  參數(shù)為函數(shù)
  def assert(predicate: () =》 Boolean) =
  if (!predicate())
  throw new RuntimeException(“assert failed”)
  assert(() =》 1 == 2)
  以上函數(shù) assert() 接收一個(gè)匿名函數(shù) () =》 1 == 2 作為參數(shù),本質(zhì)上是應(yīng)用了傳名調(diào)用的特性。
  返回值為函數(shù)
  def create(): Int =》 Int = {
  val factor = 10
  (x: Int) =》 x * factor
  }
  集合操作 Collection
  集合操作可以說(shuō)是 FP 中最常用的一個(gè)特性,激進(jìn)的 FP 擁護(hù)者甚至認(rèn)為應(yīng)該使用 foreach 替代所有循環(huán)語(yǔ)句。這些集合操作本質(zhì)上就是多個(gè)內(nèi)置的高階函數(shù)。
  例
  Scala
  val list = List(1, 2, 3)
  list.map(i =》 {
  println(s“before $i”)
  i * 2
  }).map(i =》 i + 1)
  .foreach(i =》 println(s“after $i”))
  以上定義了一個(gè)包含三個(gè)整形的列表,依次對(duì)其中每個(gè)元素乘以 2 后再加 1,最后進(jìn)行打印操作。輸出結(jié)果如下:
  before 1
  before 2
  before 3
  after 3
  after 5
  after 7
  可以看到 FP 中的集合操作關(guān)注的是數(shù)據(jù)本身,至于如何遍歷數(shù)據(jù)這一行為則是交給了語(yǔ)言內(nèi)部機(jī)制來(lái)實(shí)現(xiàn)。相比較 for 循環(huán)來(lái)說(shuō)這有兩個(gè)比較明顯的優(yōu)點(diǎn):1. 一定程度上防止了原數(shù)據(jù)被修改,2. 不用關(guān)心遍歷的順序。這樣用戶可以在必要時(shí)將操作放到多線程中而不用擔(dān)心引起一些副作用,編譯器也可以在編譯時(shí)自行對(duì)遍歷進(jìn)行深度優(yōu)化。
  Java
  List《Integer》 list = new ArrayList《》();
  list.add(1);
  list.add(2);
  list.add(3);
  list.stream()
  .map(i -》 {
  System.out.println(“before ” + i);
  return i * 2;
  }).map(i -》 i + 1)
  .forEach(i -》 System.out.println(“after ” + i));
  輸出
  before 1
  after 3
  before 2
  after 5
  before 3
  after 7
  可以從以上輸出看到對(duì)于集合操作 Scala 和 Java 的實(shí)現(xiàn)完全不一樣。
  Scala 中一個(gè)操作中的所有數(shù)據(jù)完成處理后才流向下一個(gè)操作,可以看做每個(gè)操作都是一個(gè)關(guān)卡。而 Java 則是默認(rèn)使用了惰性求值的方式,并且概念非常類似 Spark。其各種集合操作主要分為兩種: transformation 和 action。transformation 即轉(zhuǎn)換操作,所有返回 Stream 對(duì)象的函數(shù)都是 transformation 操作,該操作不會(huì)立即執(zhí)行,而是將執(zhí)行步驟保存在 Stream 對(duì)象中。action 即執(zhí)行操作,action 沒(méi)有返回值,調(diào)用后會(huì)立即執(zhí)行之前 Stream 對(duì)象中保存的所有操作。 map() 這樣的就是 transformation 操作,forEach() 就是 action 操作。
  柯理化 Currying
  柯里化指的是將一個(gè)接收多個(gè)參數(shù)的函數(shù)分解成多個(gè)接收單個(gè)參數(shù)的函數(shù)的一種技術(shù)。
  比如說(shuō)有這樣一個(gè)普通的函數(shù)
  def minus(x: Int, y: Int) = x - y
  柯理化后就變成以下形式,一個(gè)減法操作被分割為兩部分
  def minusCurrying(x: Int)(y: Int) = x - y
  調(diào)用以上兩個(gè)函數(shù)
  minus(5, 3)
  minusCurrying(5)(3)
  部分應(yīng)用 Function Partial Application
  函數(shù)的部分應(yīng)用指的是向一個(gè)接收多個(gè)參數(shù)的函數(shù)傳入部分參數(shù)從而獲得一個(gè)接收剩余參數(shù)的新函數(shù)的技術(shù)。
  比如說(shuō)有這樣一個(gè)包含多個(gè)參數(shù)的函數(shù)
  defshow(prefix: String, msg: String, postfix: String) = prefix + msg + postfix
  獲得部分應(yīng)用函數(shù)
  val applyPrefix = show(“(”, _: String, _: String)
  println(applyPrefix(“foo”, “)”)) // (foo)
  val applyPostfix = show(_: String, _: String, “)”)
  println(applyPostfix(“(”, “bar”)) // (bar)
  以上 applyPrefix() 是應(yīng)用了 show() 的第一個(gè)參數(shù)的新函數(shù),applyPostfix() 是應(yīng)用了 show() 的最后一個(gè)參數(shù)的新函數(shù)。
  偏函數(shù) Partial Function
  函數(shù)指對(duì)于所有給定類型的輸入,總是存在特定類型的輸出。
  偏函數(shù)指對(duì)于某些給定類型的輸入,可能沒(méi)有對(duì)應(yīng)的輸出,即偏函數(shù)無(wú)法處理給定類型范圍內(nèi)的所有值。
  定義一個(gè)偏函數(shù)
  val isEven: PartialFunction[Int, String] = {
  case x if x != 0 && x % 2 == 0 =》 x + “ is even”
  }
  以上 isEven() 只能處理偶數(shù),對(duì)于奇數(shù)則無(wú)法處理,所以是一個(gè)偏函數(shù)。
  偏函數(shù)可以用于責(zé)任鏈模式,每個(gè)偏函數(shù)只處理部分類型的數(shù)據(jù),其余類型的數(shù)據(jù)由下一個(gè)偏函數(shù)進(jìn)行處理。
  val isOdd: PartialFunction[Int, String] = {
  case x if x % 2 != 0 =》 x + “ is odd”
  }
  val other: PartialFunction[Int, String] = {
  case _ =》 “else”
  }
  val partial = isEven orElse isOdd orElse other
  println(partial(3)) // 3 is odd
  println(partial(0)) // else
  尾聲
  除了以上特性,函數(shù)式編程中還有 Monoid,SemiGroup 等比較難以理解的概念,本文暫時(shí)不牽扯那么深,留待有興趣的人自行調(diào)查。最后我想說(shuō)的是使用函數(shù)式編程的確很坂本,但是多了解一種編程范式對(duì)于從碼農(nóng)進(jìn)化為碼農(nóng)++還是很有幫助的。
?

非常好我支持^.^

(0) 0%

不好我反對(duì)

(0) 0%

      發(fā)表評(píng)論

      用戶評(píng)論
      評(píng)價(jià):好評(píng)中評(píng)差評(píng)

      發(fā)表評(píng)論,獲取積分! 請(qǐng)遵守相關(guān)規(guī)定!

      ?