Why Functional Programming Language?

Ray Shih
9 min readMay 31, 2017

--

這篇文章並不是要探討為什麼要學習 Functional Programming,而是要來談為什麼需要一個 Functional 程式語言來寫 Functional。

只要有在寫 JavaScript,且有在關心最新技術的話,大部分都會學過或至少聽過「學習 Functional Programming 會帶來很多好處」。而一般來說會提到 first class function、pure function、referential transparency、immutable 等等,聽起來似乎有很多東西,不過這些其實只是從不同角度來解釋、形容一兩件事情而已。

先讓我們先回頭看看這句名言:

There are only two hard things in Computer Science: cache invalidation and naming things.

似乎滿少人提到 Functional Programming 在「命名」上帶來的好處。嗯? FP 到底跟「命名」有什麼關係?上面好像也沒提到這件事情啊?事情其實是這樣的,因為命名很困難,所以 FP 的其中一個解法是:乾脆不要命名!

不需要命名

啥?不命名怎麼寫程式?好啦,其實不是不要命名,而是減少變數的使用。而方法則是從 first class function 一路推過來。

First class function 指的是你可以把 function 當作變數傳遞,也就是:

const inc = x => x + 1
const anotherInc = inc
anotherInc(1) // 2

而 higher order function 則是在這之上的延伸,例如說你可以寫一個 function 來產生 function (a.k.a. curry function):

const add = a => {
return b => a + b
}
const inc = add(1)
inc(2) // 3

除了可以回傳新的 function 以外,也可以把 function 當一般的物件,丟進另外一個 function

const compose = (f, g) => {
return x => f(g(x))
}
const inc2 = compose(inc, inc)inc2(5) // 7

這是一個 compose function,可以幫你把兩個 function 結合,而 compose function 在 lodash、ramda 之類的 utility library 中都有提供。以上的 code 如果不用 higher order function 的話,應當是寫成這樣

const inc2 = x => {
const x1 = inc(x)
const x2 = inc(x1)
return x2
}

當然你也可以這樣寫:

const inc2 = x => inc(inc(x))

不過 x 還是無法避免,試著想想多個 function 做 compose 會是什麼樣的光景。

而從這樣的例子中可以看出,其實我們可以藉由使用 higher order function 來減少變數的宣告,進而緩解命名的問題。

所以為什麼需要 functional programming language

當然 JavaScript 可以算是一種廣義的 functional programming language,不過我這邊說的 language 是指 Haskell、ELM、PureScript 這種相較之下血統更純正的語言,言下之意,JavaScript、Python、Ruby 在 functional paradigm 上其實並不是特別好用,舉例來說,假設你有以下的資料:

const dataSet = [
{values: [1, 2, 3]},
{values: [4, 5]},
]

然後你想要找出最小的最大值,也就是必須要找出每組的最大值,然後再從這些找出來的「最大值」中,找最小的值。配合 ramda library ,可以這樣寫:

const minMax = compose(
reduce(min, Infinity),
map(compose(
reduce(max, -Infinity),
prop('values')
))
)
console.log(minMax(dataSet)) // 3

一次寫對了就沒問題,但如果寫錯了呢?舉例來說,如果忘了每組 data 都需要透過 values 這個 property 才能取得資料呢?

const minMax = compose(
reduce(min, Infinity),
map(compose(
reduce(max, -Infinity),
))
)

console.log(minMax(dataSet))
// throw new TypeError('reduce: list must be array or iterable');

當然你可能會覺得這個例子太簡單了,一定是長時間睡眠不足的情況下才不小心寫出這種 bug。我當初也是這麼覺得,直到我開始大量使用這種寫法的時候,大部分 debug 時間都花在這個上面(尤其是欄位調動的時候)。我的結論是:如果大量使用 FP 在 JavaScript 上面就常常會發生這種「自己一時腦殘」,其實不是語言本身的錯,畢竟寫錯的是身為工程師自己嘛。

但是說真的,有沒有工具可以幫忙呢?有的!以 JS 來說,可以使用 flow type 這套工具,他可以 offline 幫你檢查 type signature 有沒有寫錯。But! 就是這個 But,因為 ramda 並沒有幫你做 flow type annotation,所以 flow type 預設不會檢查 ramda function 的 output,於是你以為裝了 flow type 就萬無一失,但實際上卻埋了巨大的地雷:

> flow index.js
No errors!

看來似乎完全無效啊 orz。因為 ramda 並沒有幫你加 flow type annotation,而且 flow 只會檢查檔案開頭有註明要檢查的檔案,所以 flow 碰到 ramda 就直接跳過不檢查了。

Cheng Lou 在 React Europe 2017 的講題「Imperfection」裡面有提到這個問題,有興趣可以看一下:

簡單的說,type annotation 必須要做到 100% 才能安心。也就是像 flow 這種漸進式的 annotation 方案,無法避免的一定會產生一些地雷。

不過雖然 ramda 本身並沒有套用 flow type,但可以使用 flowtyped 這套工具來安裝別人寫好的 type annotation,這樣就可以讓 flow 事先幫你檢查有沒有寫錯了!事情似乎出現了一道曙光,但其實並不如想像中順利:

index.js:20
20: map(compose(
^ function call. Function cannot be called on any member of intersection type
20: map(compose(
^^^^^^^ intersection
Member 1:
64: declare type Compose = & (<A,B,C,D,E,F,G>(fg: UnaryFn<F,G>, ef: UnaryFn<E,F>, de: UnaryFn<D,E>, cd: UnaryFn<C,D>, bc: Un
ryFn<B,C>, ab: UnaryFn<A,B>, ...rest: Array<void>) => UnaryFn<A,G>)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ polymorphic type: function type. See lib: flow-typed/npm/ramda
v0.x.x.js:64
Error:
inconsistent use of library definitions

ㄜ…..這 error 說真的我完全不想理解,不過把 code 改成這樣就可以了:

// 把 getValues 另外拿出來變成獨立的 function ,並避免使用 auto curry
const getValues = data => prop('values', data)
const minMax = compose(
reduce(min, Infinity),
map(compose(
reduce(max, -Infinity),
getValues
))
)

但這樣就比較醜,我還沒去深究原因,不過我猜是跟 ramda 幫忙做了 auto curry 而造成 type annotation 上的困難。不過更詭異的是,把 getValues 整個拿掉,反而不會有 type error。What!?拿掉很明顯就是錯的,怎麼會沒有 error ?目前我不知道要怎麼正確的解決這個問題,如果有人知道請歡迎互相交流 :)

好啦,回到題目上,為什麼我們需要一個 FP language,我們可以看看同樣的問題在 PureScript 上面會長什麼樣子:

type Data = { values :: Array Number }
minMax :: Array Data -> Number
minMax = foldl min infinity <<< map (
foldl max (negate infinity) <<< _.values
)

而如果我們不小心忘了 values 這個 property 呢?PureScript compiler 會告訴我們:

Error found:
in module Main
at src/Main.purs line 12, column 10 - line 14, column 10
Could not match type ( values :: Array Number
)
with type Number# ignore rest log

呼應到 Cheng Lou 在 ReactEurope 中提到的:type 必須要做到 100%,我們才有辦法有真正的信心說「我們沒有一時腦殘寫錯」。

結語

這篇提到了 FP 語言在對於在撰寫 functional paradigm program 上提供的幫助,我們可以借用 FP 語言所附帶的工具,來幫助我們撰寫正確的程式。可惜的是目前市場上有使用 FP 語言的工作仍舊是相對少數,不過像是 PureScript/ELM/ReasonML 都是主打 compile to JavaScript,尤其 ReasonML 由 Facebook 推行,相信 FP 的市場會越來越大。

當然除了 Type Check 以外還有很多很多 FPL feature 可以幫助我們撰寫 FP 程式。Stay Tuned!

P.S. 其實並不是所有「血統純正」的 FPL 都有解決這個問題

Thanks Wu, Ching Ting, Tom Chen for reviewing.

--

--

Ray Shih

Functional Programming Advocator, Haskell, Idris, PureScript Lover. Work at Facebook and Machine Learning student.