Andy
Andy

手民

編程自學指南二・公平的過程能否保證平等的結果?

經過上一章,希望大家多少對「寫代碼」這件事有點直觀感覺了。不過,改網頁背景沒什麼意思,下面,我們用編程來研究個哲學問題。

我們要研究的哲學問題是:

公平的過程能否保證平等的結果?(等價的逆否命題爲:觀察到不平等的結果,是否可以以此定論過程不公平?)

一、第一輪抽象——量化

這個問題很難下手,我們可以把問題量化,比如可以細化爲:

(1) 用個人資產數額爲公平和平等的量度標準,

(2) 如果社會裏所有人起始資產相同,

(3) 交易過程完全公平(比如說每天扔骰子隨機抽兩個人,從甲的錢包裏拿1元給乙),

(4) 經過很多輪交易之後,

社會裏的人的財富餘額會相同嗎?

(當然,我並不是想說平等問題只有這一種量化方法。這樣定義只是因爲比較方便後面的步驟。)

二、第二輪抽象——數位化

上面的問題,其實在物理世界就可以模擬。

比如,我們可以假設有6個人,編號爲1至6,然後在紙上畫個表,記錄6個人的資產量,一開始都設爲100好了。然後投擲骰子兩次,第一個擲出的數字是出資方,第二擲出的數字編號是接受方,然後出資方資產-1,接受方資產+1。(如果第一、第二數字相同,則所有人資產不變)。如此玩若干輪,就能夠知道,完全隨機的財富分配會產生什麼樣的財富分佈。

但是,這篇文章是編程教程,所以我們要用電子數位計算機來模擬這個過程。

下面,請大家打開瀏覽器的控制臺(方法上一章有)。以下編程語句都是輸入控制臺執行。

1. 模擬資產

我們有6個人,就需要六個名字,否則會無法區分。我們可以先把他們的資產數額的代號記作a-f。

那麼,賦予他們初始資產的語句爲:

a = 100

b = 100

c = 100

d = 100

e = 100

f = 100

2. 模擬骰子

然後,我們需要用編程語言模擬一個六面骰,即一個生成範圍爲1-6整數的隨機數生成器。

JavaScript並沒有這樣的「骰子功能」,所以我們需要自己造一個。

JavaScript原生的「隨機數」*的產生器是Math.random函數。大家可以試試運行幾次這個函數†:

Math.random()

可以看到,每次運行的輸出都不一樣。實際上Math.random函數會輸出一個0-1之間的小數。而我們需要1-6之間的整數,所以再需要兩個操作:把值域從0-1映射到1-6(用乘法和加法),以及把小數轉換爲整數〔用Math.floor函數,它會把小數點後的部分扔掉,例如Math.floor(3.85)是3〕。具體來說,就是:

1 + Math.floor(Math.random() * 6)

大家可以再試試執行這個語句幾次:

這個,就是我們的「骰子」。

* 其實也就是「熵・entropy」。大多數編程語言服從「機械決定論」,像隨機數這樣事先無法確定輸出的函數,是很罕見的。

† Math.random和Math.random()是不一樣的意思。前者是個名字,指代Math.random這個函數本身,後者是「執行」這個函數。

3. 模擬交易過程

好,下面我們來模擬交易過程。(由於你的「骰子」跟我的「骰子」擲出來的數字大概不一樣,你的模擬結果大概跟我也不同。)

首先我投了兩次骰子,第一次投出6,第二次投出3,也就是第一輪交易由6號玩家(f)給1元錢給3號玩家(c)。

f = f - 1 的意思是,給 f 這個名字以新的意義,這個新意義是 f 現在意義(某個數)減一。c = c + 1 同理。

第二輪骰子投出了5和3,就是e要給c玩家1元。

三、第三輪抽象——自動化

你可能要說了,這可太慢了,這要算到天荒地老才能看出結果啊。

沒錯,這是因爲我們還沒有動用編程語言的強項:抽象化、結構化與自動化。

1. 抽象化投骰

1 + Math.floor(Math.random() * 6),每次用都要複製粘貼,非常麻煩,也很容易出錯。我們要給它一個名字,就可以每次直接用它了:

擲骰 = () => 1 + Math.floor(Math.random() * 6)

留意,多了一個 () => 這組符號。這組符號的意思是「定義函數」。

所謂函數,是指把輸入轉換爲輸出的運算機制( 也就是「=>」這個「箭頭」的意義)。箭頭之前的一對括號「()」裏面放的是函數的輸入(又稱參數),隨機數這種函數比較特殊,它沒有輸入(所以第一對括號是空的)。箭頭之後的內容就是「輸出」。

這樣,「抽象化」了「投骰」這個概念(它從一個具象的運算,變成了一個抽象的名字)。

2. 結構化資產記錄

現在,玩家的資產是記錄在a-f這幾個名字裏面的,其實這非常麻煩,因爲骰子擲出來的是1-6,並不是a-f,也就是我們要手動把1-6「翻譯成」a-f,很麻煩。最方便的當然是直接用1-6作各個玩家的名字,然而,這會引發更嚴重的問題,就是原來1-6代表的數值(自然數一、二、三……)就沒有名字表示了。

所幸,JavaScript提供了一種結構,允許我們用數碼作編號,存取數據。這個結構,英語叫array,漢譯一般是「數組」,但這個翻譯不是非常好,因爲「數組」不一定存儲「數」。

array用中括號創造,比如:

[100, 100, 100, 100, 100, 100]

這是一個有6個元素的數組。

也可以給數組取個名字:

玩家財富 = [100, 100, 100, 100, 100, 100]

讀取其中某個元素的數據,用的也是中括號。JavaScript的數組編號是從0開始數的(有別於日常生活大家用1開始數)。

所以,第1個玩家的財富數額爲:

玩家財富[0]

數組的內容也是可以改的,比如給6號玩家(數組序號5)加50元:

玩家財富[5] = 玩家財富[5] + 50

3. 自動化交易

剛纔我們還遇到了一個問題,就是每輪模擬都必須手動輸入代碼執行運算,實在是太痛苦了。

JavaScript也爲我們提供了一個執行重複操作的機制:循環。

循環有很多種,我們這裏只介紹「for循環」,舉例:

for (循環輪數 = 0; 循環輪數 < 5; 循環輪數 = 循環輪數 + 1) {

alert(循環輪數)

}

它的意思是,先設爲「循環輪數」0,執行花括號「{}」內的內容一次(alert是彈窗提示,提示內容爲括號內的內容),然後讓「循環輪數」加一,然後判斷「循環輪數 < 5」是否還成立,如果成立,那就繼續執行一輪,如果不成立,那就退出循環。

你應該會看到這樣的窗口彈出5次:

綜合以上機制,我們可以得到一個自動交易模擬機:

擲骰 = () => 1 + Math.floor(Math.random() * 6)

玩家財富 = [100, 100, 100, 100, 100, 100]

for (日期 = 0; 日期 < 3652; 日期 = 日期 + 1) {

出資方編號 = 擲骰() - 1

受資方編號 = 擲骰() - 1

玩家財富[出資方編號] = 玩家財富[出資方編號] - 1

玩家財富[受資方編號] = 玩家財富[受資方編號] + 1

}

玩家財富

(在控制臺中按鍵盤上的「上箭頭」可以調出之前執行的指令,可以馬上再玩一次)

最後的數組輸出就是3652天後,各玩家的財富值。

四、具象化

我們剛纔把現實問題經過三層抽象化(量化・數位化・自動化),變成了可以在瀏覽器裏自動模擬的問題,現在我們得到了模擬輸出,就可以逆轉抽象化,推斷模擬輸出在物理世界的意義。

結果是,即使開始條件完全一致(100元),每天的交易也是隨機的(由6面骰抽兩個人作財富零和轉移),經過10年(3652天),最有錢的玩家和最窮的玩家之間,還是形成了2倍至3倍的財富差距。

換句話說,「結果不平等」並不是「過程不公平」的充分條件。即使財富分配過程是絕對平均的,財富的分佈依然會不平均。

五、參數化(再抽象化)

上一節我們已經解決了我們要解決的問題,但是只怕引出了更多問題:如果模擬時間是100年如何?如果模擬時間是1個月如何?如果一次轉出10元如何?如果不是零和,而是交易會產生價值如何……?如果有1000人會如何?

當然我們可以手動修改我們的模擬機程序,但是這樣很麻煩,也容易出錯。

我們需要的,是把模擬機中的「可變部分」命名,然後就可以單獨控制他們了(被抽出的部分稱爲「參數」)。

還記得我們剛纔做的「投骰」函數嗎?我們這次做點更精緻的抽象化。

1. 抽象化模擬機

我們先抽象化模擬機,給它一個名字:

模擬財富再分配 = () => {

擲骰 = () => 1 + Math.floor(Math.random() * 6)

玩家財富 = [100, 100, 100, 100, 100, 100]

for (日期 = 0; 日期 < 3652; 日期 = 日期 + 1) {

出資方編號 = 擲骰() - 1

受資方編號 = 擲骰() - 1

玩家財富[出資方編號] = 玩家財富[出資方編號] - 1

玩家財富[受資方編號] = 玩家財富[受資方編號] + 1

}

console.log(玩家財富)

}

是不是方便多了?

2. 抽象化函數參數

剛纔我們抽象出來的模擬機函數,很多變數都是直接寫在代碼裏的,沒有名字,比如

玩家財富[受資方編號] = 玩家財富[受資方編號] + 1

這個「1」,行話叫「寫死」的,就是只能是阿拉伯數字1,沒有名字,不好改。

我們可以把我們想嘗試改的量,都取上名字,這樣改起來就容易了。

取名字的方法是,在箭頭(「=>」)前的括號中,寫上名字,然後在函數的定義裏面,用名字而非數字。

就像這樣:

模擬財富再分配參數版 = (人數, 初始財富, 總週期, 出資方變化, 受資方變化) => {

擲骰 = () => 1 + Math.floor(Math.random() * 人數)

玩家財富 = []

for (玩家編號 = 0; 玩家編號 < 人數; 玩家編號 = 玩家編號 + 1) {

玩家財富[玩家編號] = 初始財富

}

for (日期 = 0; 日期 < 總週期; 日期 = 日期 + 1) {

出資方編號 = 擲骰() - 1

受資方編號 = 擲骰() - 1

玩家財富[出資方編號] = 玩家財富[出資方編號] + 出資方變化

玩家財富[受資方編號] = 玩家財富[受資方編號] + 受資方變化

}

console.log(玩家財富)

}

那麼,我們剛纔模擬的情況(6個人,每人起始資金100元,一共3652日,出資方減少1元,受資方增加1元),就被抽象爲:

模擬財富再分配參數版(6, 100, 3652, -1, 1)

這樣,花樣就多了,比如:——

正和遊戲(出資方-1元,受資方2元):

模擬財富再分配參數版(6, 100, 3652, -1, 2)

零和遊戲,但是運行一個世紀(36524日):

模擬財富再分配參數版(6, 100, 36524, -1, 1)

零和遊戲,但是10個人玩:

模擬財富再分配參數版(10, 100, 3652, -1, 1)

可以得到的具象結論有:即使是正和遊戲,結果一樣不平等。但結果不平等或許較低。零和遊戲長了,有人要破產(理所當然?)。人多一點,似乎結果不平等程度會降低?

六、直覺化(另類具象化)

上面第四節的具象化,用的是數值的思路,主攻人的理性。但人當然也不是全然理性的,換句話說,走直覺的路徑向人腦輸入信息,有時候會更有效。五官六感,視覺強勢,於是我們可以做「視覺化」(visualization;又譯「可視化」,不好,並不是把原本看不見的東西變成看得見),而視覺化正是JavaScript的強項。

具體代碼涉及的知識比較多,這次就不細講了。

貼張截圖:

動態版可以在這裏玩到:

https://blaesus.github.io/random-handout/

代碼(有大量的視覺化操作,實際隨機過程模擬倒是很少):

https://github.com/blaesus/random-handout/blob/master/main.js#L43

七、數學化(另類抽象化)

目前我們用的是都是數值模擬的方法來處理隨機過程。它的問題是,你並不知道,你得到的結果,是「典型的」,還是只是恰好骰子扔出了一種「特別的」情況。

要處理這個問題,我們可以算很多次,然後平均多次的結果,從而得出相對「典型」的結果。

也可以在量化問題之後,不做數位化,而是用統計概念去處理。

這個財富分配的過程可以理解爲馬爾卡夫過程,然後或許可以直接推算出時間趨向於無窮時,財富的「期望」分佈函數,從而繞過所有的具體計算。按說這種抽象運算,也有軟件可以做,比如Mathematica什麼的,然而我不會,所以只能說到這裏了。

八、本章的新概念

總結,本章引入了以下新概念:

抽象化

把一堆互相有關係的東西打包,取個名字,以後就只用名字支撐,不管包袱裏面有什麼。

結構化

把有關的數據放在一個名字下,而不是每個數據一個名字。

自動化

把需要人類觸發才執行的操作,變成自己觸發自己,或者說,編程上一個操作觸發下一個操作,從而不需要人類不斷觸發。

參數化

是抽象化的一種。有時候,需要抽象化的那堆東西,有些不變,有些經常變化,可以把經常變化的東西隔離出來,取個名字,方便操作。抽象出來有名字的、經常變化的東西,叫做參數。

具象化

抽象化的反操作,把數位世界的概念還原爲物理世界的概念,或者把數值轉化爲人類感知等。

本章介紹的JavaSript新操作有:

取名

名字 = 名字的意義

定義函數

(輸入1, 輸入2) => 輸出

定義函數然後取名

函數名字 = (輸入1, 輸入2) => 輸出

指代函數

函數名字

調用函數

函數名字(輸入量,另一個輸入量)

調用不依賴輸入就有輸出的函數

函數名字()

數組

array = [1, 2, 3]

array[2] = array[2] + 10

for循環

for (起始條件; 繼續條件; 條件變化指令) {

循環內容

}

CC BY-NC-ND 2.0 版权声明

喜欢我的文章吗?
别忘了给点支持与赞赏,让我知道创作的路上有你陪伴。

加载中…
加载中…

发布评论