在函數範式中構建程序

什麼是函數範式

函數式編程(英語:functional programming)
以λ演算爲理論基礎的編程範型, 將電腦運算視爲數學上的函數計算, 而且避免使用程序狀態以及易變對象.算法

比起命令式編程,函數式編程更增強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而不是設計一個複雜的執行過程。

電腦運算視爲數學上的函數計算

數學上的函數只要遵照計算準則, 輸入和輸出是肯定的, 是能夠被組合、能夠被嚴格推理的編程

$$f(x)=ax^2+bx+c$$數組

複雜的函數是由簡單的函數組合而成的, 函數範式也是由簡單運算一層層組合爲複雜執行過程的安全

但電腦運算最大的障礙是電腦程序是會有反作用網絡

所以函數範式的核心問題便是如何處理這些反作用數據結構


反作用

  • 變量
  • IO操做(文件讀寫、網絡操做等)
  • 線程操做
  • 與系統的交互(GUI、硬件交互等)

強類型

數學中函數是有嚴格定義域的, 即值的定義範圍, 映射到程序中就是類型框架

換句話說咱們定義一個類型實際就是在定義一個值的範圍("範圍"這個詞可能不太準確,可能更相似「枚舉」,好比「True」和「False」兩個枚舉的集合就是Boolean類型)數據結構和算法

電腦運算視爲數學上的函數計算另外一個過程便是定義域的轉換, 若是數學函數須要嚴格的定義域, 程序中須要嚴格的類型限定也是很天然的事情ide

fun plusOne(a: Int): Int
函數名 定義域 值域
plusOne a: Int : Int
高階類型、逆變協變、類型別名

從數據結構開始構建程序

程序 = 數據結構 + 算法

OOP實際不少時候將數據結構和算法混雜到了一塊兒, 函數範式則迴歸本源將二者分離:函數式編程

函數式程序 = 數據結構 + 算法

程序編寫開始以前, 首先定義數據結構


函數式數據結構

OOP提倡針對不一樣問題定義不一樣的數據結構, 這致使定義的數據結構一般不通用, 好比不一樣XML解析庫會定義不一樣的專用數據類型

函數範式則不一樣, 它們用不多一組關鍵數據結構(如list、set、map)來搭配專爲這些數據結構深度優化過的操做。

在這些關鍵數據結構和操做構成的一套運起色構上,按須要插入另外的數據結構和高階函數來調整以適應具體問題。


Example

創造值域:

sealed class ItemData

data class ItemListData(...) : ItemData()

data class ItemOptionData(...) : ItemData()
List<ItemData>
typealias Selecable<T> = Pair<T, Boolean>

List<Selecable<ItemData>>

不可變數據類型

定義代數數據類型的一個基本限制是其中不能有變元, 這就是代數的的含義

因此咱們在函數範式中只能定義不可變數據

好比鏈接兩個list會產生一個新的list,對輸入的兩個list不作改變。

從另外一方面來解釋這種限制:
變量的存在每每是程序Bug的最終來源, 它的值依賴於運行時(線程切換狀態、用戶操做順序、系統狀態等等), 沒法在測試期徹底限制掉, 墨菲定律


函數式數據結構中的數據共享

當咱們對一個已存在的列表xs在前面添加一個元素1的時候,返回一個新的列表,即Cons(1, xs),既然列表是不可變的,咱們不須要去複製一份xs,能夠直接複用它,這稱爲數據共享

共享不可變數據可讓函數實現更高的效率。咱們能夠返回不可變數據結構而不用擔憂後續代碼修改它,不須要悲觀地複製一份以免對其修改或污染。

全部關於更高效支持不一樣操做方式的純函數式數據結構,其實都是找到一種聰明的方式來利用數據共享。

正因如此,在大型程序裏,函數式編程每每能取得比依賴反作用更好的性能。


用組合代替繼承

OOP中算法和數據結構是混合在一塊兒放在叫作的東西里, 所以對算法(一般算法須要操做數據結構)的擴展須要經過繼承的方式來實現, 這種處理方式有兩個問題:

  • 多繼承問題、難以組合
  • 難以額外擴展
  • 方法能夠被複寫意味着方法是可變
eg: 兩種實現了不一樣功能的Activity繼承擴展類沒法被組合

回溯上文, 咱們說函數式程序 = 數據結構 + 算法, 數據結構被單獨定義後, 算法就被獨立出來了, 所以函數範式採用了更加靈活的方式組合這些算法


類型類

這些被分離出來的算法不一樣於OOP中的, 因此函數範式中將之稱之爲類型類(typeclass)

因爲數據自己不可變, 因此不用像OOP同樣須要嚴格 private內聚一些內部方法以防止內部變量被非法操做後數據變得不符合預期
所以函數範式中方法默認是 public

Example

interface Eq<in F> {
  fun F.eqv(b: F): Boolean
}
interface Show<in A> {
  fun A.show(): String
}
interface ShowEq<in A> : Eq<A>, Show<A> {
    override fun A.show(): String = this.toString()
}
interface CharEqInstance : Eq<Char> {
  override fun Char.eqv(b: Char): Boolean = this == b
}

fun Char.Companion.eq(): Eq<Char> =
  object : CharEqInstance {}

QuickCheck

咱們定義一個方法(函數)實際上就是在定義一個數學上的函數, 所以它應該是能夠被嚴格驗證的, 即指定定義域上值應該都能被映射到指定的值域

測試在數學意義上就是作這個驗證的, 輸入全部定義域上的可能值驗證是否正確映射到了值域

傳統的測試實際並無覆蓋全定義域, 所以可能致使某些額外狀況. 函數範式中經常使用的測試框架QuickCheck便是嘗試作全覆蓋測試

這也是爲何提倡對函數參數進行強類型化, 沒有足夠的類型限制實際就是擴展了函數的定義域, 這通常有兩種狀況:

  1. 假定了輸入數據的子域(好比參數是String, 卻假定格式爲1.1.2)
  2. 包含了實際本身處理不了的數據(好比不能處理空數據, 卻標明能夠爲空)

甚至函數對象也是能夠被生成的


Example

object EqLaws {
  inline fun <F> laws(EQ: Eq<F>, noinline cf: (Int) -> F): List<Law> =
    listOf(
      Law("Eq Laws: identity") { EQ.identityEquality(cf) },
      Law("Eq Laws: commutativity") { EQ.commutativeEquality(cf) }
    )

  fun <F> Eq<F>.identityEquality(cf: (Int) -> F): Unit =
    forAll(Gen.int()) { int: Int ->
      val a = cf(int)
      a.eqv(a) == a.eqv(a)
    }

  fun <F> Eq<F>.commutativeEquality(cf: (Int) -> F): Unit =
    forAll(Gen.int()) { int: Int ->
      val a = cf(int)
      val b = cf(int)
      a.eqv(b) == b.eqv(a)
    }
}

反作用的分離

反作用在程序中是確實存在的, 問題是如何將之分離出去, 函數範式的處理方式能夠看作建造一個管道, 管道自己是代數的肯定的, 而其中流淌的就是反作用

這些管道常見的有:
IOSingleMaybe

對於必須存在的變量的處理方法就是, 將之放到安全的地方, 通常而言即線程安全


IO

IO不一樣於咱們一般意義上的input/output操做的意思, 它是Haskell中用來抽象外部做用的特殊數據結構, 它隱含了一個叫RealWorld的上下文用於和外部環境進行交互(即反作用), 全部的反作用必須包含在其中


Example

Haskell:

putStrLn :: String -> IO ()

getLine :: IO String

main = do
  name <- getLine
  putStrLn ("Hey, " ++ name)

Kotlin:

fun putStrLn(s: String): IO<Unit>

fun getLine(): IO<String>
IO.monad().binding {
  val s = readString().bind()
  putStrLn(s).bind()
}

Rx:

fun putStrLn(s: String): Completable

fun getLine(): Single<String>
fun main() = getLine()
  .flatMapCompletable { s -> putStrLn(s) }
main().blockingAwait()
相關文章
相關標籤/搜索