iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 13
1

我將真相刻在鋼板上,其餘的皆不可信。

-- 布蘭登·山德森, 迷霧之子:昇華之井


我注意到窗外有個告示板,或是加油站,又或是同為兩者的東西

告示板上寫著大大的數字,三不五時就會有像是會移動的盒子靠到看板下方,接著會從盒子那邊,伸出兩條平行的光管聯結看板,接著可以發現盒子中,裝載了看板當時顯示的數字。盒子收回光管後就離開了。

偶爾,也會看到告示牌伸出光管,橫跨天空向著遠處的什麼東西伸去,而之後板子上的數字就會換成另一個數字。

我問小動物這些到底是發生了什麼事。

小動物說這叫綁定啊,然後一付想起什麼的表情,叫我試著用迴圈去加總個一到十試試。

想說這個做過幾百次了不要浪費時間吧,但我還是試了。

這什麼鬼東西。


沒反應的迴圈

accu = 0

for i <- 1..10 do
  accu = accu + i
end

accu #=> 0

「我應該沒有寫錯什麼吧?這根本沒有動啊,為什麼 accu 最後還是 0?」

語法上是可執行的沒錯,但是在這些城市裡,變數不是這樣用的。你只是太習慣比較大的都市裡那個等號是指派的規則了,但那並不是唯一可用的規則。

我們再回頭想想數學,在數學裡,一旦你說 x = 0,那麼 x 在這個語境裡,就永遠是 0 了。仔細想一下,如果你把 = 想成「之後用左邊的符號代表右邊的值」, x = x + 1 會是一個有點矛盾的語句。你想說的其實是「要用 x 符號來代表之前的x符號代表的值加上 1。」

若是在那個叫 Erlang 的都市裡,當你寫 X = X + 1 時,它會直接跳出錯誤說「這個變數符號已經有講好的意義了,不能再改了。剛剛講好 X0,現在又說是 1,你是騙子嗎?」 (其實沒有後半句,或至少它不會印出來給你看)

但在 Elixir 裡,為了使用方便,他們做出了一點妥協。但…只有一點點而己。在這裡,變數是綁定,而非指派的

其它城市,指派的情況

而這兩者的差異,就讓我來畫給你看吧:

在其它的城市裡,當我們寫了 x = 0,是意味著把變數 x 指派成 0。而這時在魔法運作的記憶區塊裡會新增一個區塊,接著將 x 這個變數連結到區塊,並在這個區塊內寫入 0的值。

https://ithelp.ithome.com.tw/upload/images/20200928/20103390IDqL2JZTFv.png

而當我們再寫下 x = 10 時,我們會找到這個區塊,並將裡面寫的值直接改成 10

https://ithelp.ithome.com.tw/upload/images/20200928/20103390CbHN0b1hRv.png

不會變的…變數

而在 Elixir 之城裡,當我們寫了 x = 0,一樣會在記憶區裡新增區塊並連結,而在區塊內寫入 0 的值,到此為止都是一樣的。

https://ithelp.ithome.com.tw/upload/images/20200928/20103390KB24W2Ai5Z.png

但若你重新綁定 x 為 10 的時候,術法會在記憶體區裡再新增一個區塊寫值並連結,而原先的區塊保持不變

https://ithelp.ithome.com.tw/upload/images/20200928/20103390lcNNhI1dxi.png

在 Elixir 裡,變數不是被指派了一個值,而是與某個值綁定,而它也可以改成綁定其它的變數。但原先的值一旦寫下就寫下了,會保持在那不變,我們稱這種行為叫 immutable,不可變動的。有了這個特性,我們可以來談一下某個在其它國度裡很令人困惑的東西了…

函式內引用了外界變數

讓我們先定義一個外界變數,並且寫一個匿名函式,讓它向外引用變數。接著改變這個變數的值,在其它的國家裡,會發生什麼事呢?在 JS 莊園裡,會是這樣的:

let x = 10

function foo(y) { return x + y }
foo(1) //=> 11

x = 99
foo(1) //=> 100

由於在 foo 的定義裡,並沒有 x 這參數,所以在 JS 莊園裡的函式會向上找看有沒有可用的 x 的定義。這次它找到了,是 10

https://ithelp.ithome.com.tw/upload/images/20200928/20103390iROrXMdD37.png

於是在第一次 foo(1) 呼叫時,我們可以得到 10 + 1,也就是 11

但接著我們把 x 改成 99,並重新呼叫一次,這時我們會拿到 99 + 1 的結果,也就是 100

https://ithelp.ithome.com.tw/upload/images/20200928/20103390yFDXz35YwH.png

這個情況,在 Ruby 公國裡也一樣:

# ruby 語法
x = 10

foo = lambda {|y| x + y}
foo.(1) #=> 11

x = 99
foo.(1) #=> 100

有時人們設計的魔法依賴這種方式運作,而有時,則會造成困擾。

(我想起之前想要在網頁裡用迴圈綁定事件,結果按按鈕都出現一樣的值的窘境)

因為用一樣的引數呼叫同一個函式兩次,卻出現了不一樣的結果

把值…封裝起來

那麼在 Elixir 之城裡,一樣的寫法,會發生什麼事呢?

x = 10
foo = fn y -> x + y end
foo.(1) #=> 11

x = 99
foo.(1) #=> 11

一樣我們定義了變數 x 與引用它的匿名函式 foo。先呼叫一次可以拿到 11

https://ithelp.ithome.com.tw/upload/images/20200928/20103390TL5zwzyXcE.png

但當我們重新綁定 x時,由於只是改變了外面那個 x 的繫結目標,所以已定義的函式,依然是使用原先綁定的那個值。所以每次呼叫函式時,只要給了一樣的引數,那永遠會有一樣的回傳值

https://ithelp.ithome.com.tw/upload/images/20200928/20103390ZSxoNSfXRF.png

這就是所謂 閉包 (closure) 的本質,只要函式引用了外界變數,就是一個閉包。然而在函數式的語境裡,這個函式會將其當下的值封存起來,之後不管原先那個外界變數如何改變,每次用一樣的引數呼叫這個函式,都會拿到相同的結果,而這個行為,比較是數學上認為函數該有的樣子。

就像你們世界裡的琥珀化石一樣,一旦樹脂包住昆蟲或蠍子或植物後,它就保持著封存那一瞬間的姿態了。

怎麼在其它環境模仿這個行為

而在其它選擇了 mutable 語境的國度裡,想要模仿這個行為,該怎麼辦呢?在那些地方,常常會採用回傳函式的函式這個手法,來製作出有相同行為的函式:

let x = 10

let bar = function(x) {
  return function(y) { return x + y }
}
let foo = bar(x)
foo(1); //=> 11

x = 99
foo(1) //=> 11

我們先定義 bar 這個會回傳函式的函式,接受一個參數,並回傳一個函式。而回傳的函式裡引用到的參數,是呼叫 bar 當下的值

而當我們呼叫 bar 傳入 x 時,拿到的回傳的函式 foo,就會永遠引用那個 10 的值了。

就算我們之後再來改變外界的 x,也已經跟呼叫 bar 那時無關了。

取捨

這些,都是在設計一個國度時的取捨。當然沒有了「可以改變內容的變數」,在操作上就看似相當受限。因此在這些國度裡,遞迴是非常非常重要的,他可以做到其它的地方用共有的可改變變數所做的事。但在採用了這個設計後,我們交換到的,是更容易除錯與測試,更容易平行化,以及最重要的,更貼近數學上的概念

「所以,在這些語言…嗯…國度裡,等號就是變數綁定嗎?」

還要再更棒一些。

[to be continue]


上一篇
mostly:functional 第十一章:冗餘的變數,連續的轉變
下一篇
mostly:functional 第十三章:當我們談論等號時,我們在談論什麼?*
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言