2014年11月21日 星期五

Jonathan Blow's Ideas About Programming Language雜記(3)-Demo






前兩個Talk之後約莫一個多月的時間,Jonathan Blow居然展示了一個新的程式語言compiler雛形,這個新語言之中實現了許多之前提到的想法,而且還真的可以Run,上圖就是用此程式語言寫的invaders。據說明年1月就會有初期版本公開釋出。這次Demo內容timeline如下。

Talk #3 Demo



5:30

import,load syntax
load用來load source file,有點像include
This compiler is self contained, IDEs are not needed.

9:00

compile in command line: jai xxx.jai         (目前副檔名是.jai,""語言XD?)
目前backend 是cl

9:40

simple operations

14:48

no implicit type conversion

15:10

function declaration
struct syntax
memory ownership
initial value
no default value now
array declaration

16:53

enum(named/unamed/typed/type infered)
out of order reference:
VALUE = MIDDLE_VALUE
MIDDLE_VALUE :=8

20:16

testproc()

21:10

new/delete

22:48

defer (GO has it too.)
defer demo

25:34

memory ownership

26:35

printf (through FFI to C)

27:10

named lambda

28:7

for n: 1..count {...}


29:29

dynamic array
(temporary)

30:30

for loops over dynamic array

32:30

character '(single qoute)
string ""(double qouted)
more "fors"

33:40

invaders demo
No sound
Using OPENGL


35:15

It's a real compiler though it output C
Output our own errors instead of C compiler error.
Rely on "cl" to do optimization.


38: 30

Custom check call(compile time)
Compile-time checking call using same syntax as run time language.

40:55

also compile to byte code in addition to output C codes (two compile target)

43:35

string type has bugs now.

44:05

More compile time execution
#run syntax
Invaders at compile time!!
Everything you can do at runtime you can do in compile time.
Load dlls when compile to bytecode.

48:10

Pasting local function to global without changing anything

49:07

#run local function at compile time
Bugs now!
#run invaders() in local scope at compile time.
How many invaders you defeated at compile time?

51:20  (MUST WATCH!!)

#assert syntax
"You are terrible, You must defeat at least 10 invaders to compile!"

53:05

More #assert cases
ASCII map format checking

58:58

Dependency and Building
Compiler knows the dependency

1:02:00

Conditional building
Preprocessor in runtime/compile-time language

1:12:01

BYTE CODES formats

1:16:40 

Linux version


(QA省略)

2014年11月20日 星期四

Jonathan Blow's Ideas About Programming Language雜記(2)



在上一個Talk中,獨立遊戲Braid製作人Jonathan Blow道出了針對高效能遊戲開發之程式語言的許多想法。Talk #2接續上個演講,有許多想法可能跟之前有重疊,但這次重點放在各種宣告方式(Declaration)與其對重構性(Factorability)的影響,在影片後段也提到了C++中lambda的Capture可以如何改進。部分觀念極大地挑戰了過去大家對編程方法最佳實踐(Best practice)的認知,整體來說依然精彩,筆記整理如下。

Talk #2 : Declarations and Factorability

Preface

在這個Talk的標題中,使用的是"Factorability"而非"Refactrobility",因為若是用Refactorbility聽起來好像在說:"Oh,我的Code寫的好爛,應該要來重構(Refactorbility)一下。"這不是我們所要討論的情形,我(Jonathan)想強調的是在開發的過程中,Code Base自然而然會經歷大大小小的增修,Code區塊到處移動的此種流動的現象,在我們設計程式語言時有哪些方式能支援,因此使用Factorbility是比較恰當的。

在上一個Talk中,花了那麼多時間,我想表達一個概念:傳統上被認為是best practice的程式設計法則遊戲程式設計師的實際經驗有巨大的鴻溝。這不是說我們這些寫遊戲程式的全部一致同意這想法,我們過去也曾經遵守這些best practice,但依我們長久以來的經驗,加上與許多程式設計同儕討論之後,我們"基本上同意",這些所謂的best practice裡面至少有一部分對程式設計而言反而是有破壞性的。

Insomniac公司的Engine Director-Mike Acton在CppCon 2014上的這個Talk就傳達了類似上述想法,大家可以參考看看。

而有一部分的程式語言,在設計上可能會強迫使用者去遵守某些best practice,例如Safety之類的,以我們的經驗來說,這樣子的設計太過理想化。可能有些程式語言的狂熱支持者,如C++、D或Rust粉絲會認為我又在大放厥詞,或者我根本就不懂這些程式語言,但實際情況並非如此。

What I Learned In Programming School

inline與函數

請看下面這段Code:

上半段的MajorFunction裡註解的意思是把MinorFunction1~3的內容inline寫進Code裡。

在學校裡,教授會教我們說:每當你要寫一個很長的函數(MajorFunction)的時候,要把可重覆使用的部分另外獨立成一個函數(MinorFunction1~3)。這麼做有許多好處,第一,獨立的函數比較好理解,以函數層級去理解整個程式也會比較好理解,因為有許多瑣碎的操作被封裝了,你可以用高階一點的概念去思考整個程式。第二,你增加了程式的重用性,因為這些獨立的函數可以被重複使用。第三,獨立的函數裡的操作,不會汙染到其他函數,減少了互相影響導致出錯的機會。基於以上優點,教授會強烈建議你一開始就把函數寫成下半部那樣,或是當你發現可以寫成小函數就馬上寫成小函數。

聽起來似乎很有道理,但是基於過去經驗,我開始了解這樣做是錯的。

為什麼?
當你把操作寫成 小函數,或許這個小函數本身真的比較好理解,但是對整個程式來說,你為這個程式引入了新的概念(i.e.你剛獨立出來的小函數),如果類似這種小函數越多,整體程式而言概念也就越多,而一個程式裡的函數越多,程式越難理解,因為本來直接了當的概念(inline)變的間接隱晦(MinorFunction)了。

為了理解整個程式你必須理解這些小函數內部的操作,還有他們之間的互動關係。而當概念變的間接隱晦,你會更難理解概念之間的互動,你必須把這些小概念在做什麼都記在你有限的短期記憶裡,這樣會讓你更難寫程式。

把操作寫成小函數為整個程式帶來哪些負擔呢?
舉例來說,當你把函數獨立出來,那表示整個程式都有可能可以呼叫這個函數,"哪些東西呼叫了這個函數?" 這就是你所必須多花費心力考慮的地方,你可以用IDE或editor幫助你解決這個問題,但顯然這樣讓事情變得更麻煩。

再來你的函數何時會被呼叫?被呼叫時必須滿足哪些前提?當你只是把操作inline寫在某個區塊裡,你所需要考慮的state就在這個區塊裡而已,但是當你把這些操作fator成小函數,你所需要考慮的就是整個程式的state變化了。

當然你可以用註解來減緩這個問題,但對程式設計師而言,寫正確的註解並非易事。且對程式而言,註解就是沒被跑過的Code,而我們知道沒被跑過的Code通常很有可能有Bug,而且你沒有有效的方法去驗證註解本身的正確性,因此註解幫助不大。

John Carmack的內部信

過去數年的程式設計經驗讓我開始有了上述想法,但我還不是非常肯定。直到有一天我讀了John Carmack在2007年寫給Id software內部員工的一封email:
The flight control code for the Armadillo rockets is only a few thousand lines of code, so I took the main tic function and started inlining all the subroutines. While I can't say that I found a hidden bug that could have caused a crash (literally...), I did find several variables that were set multiple times, a couple control flow things that looked a bit dodgy, and the final code got smaller and cleaner.
John Carmack說他曾經嘗試把原本是小函數的Code inline寫進main tick function,結果雖然沒有找到隱藏的足以導致火箭爆炸的bug,但是他發現有些變數被多次賦值,還有一些隱晦的流程控制,並且最後Code變得更精簡明晰。

其中跟遊戲相關的(當時John正進行Rage的開發):
If something is going to be done once per frame, there is some value to having it happen in the outermost part of the frame loop, rather than buried deep inside some chain of functions that may wind up getting skipped for some reason.
John Carmack:如果有些事情你每個frame都要做,與其讓這些事情深埋在一連串基於某些原因可能跳過這些事情的函數中,倒不如就直接把這些事情inline在main loop中。
Besides awareness of the actual code being executed, inlining functions also has the benefit of not making it possible to call the function from other places. That sounds ridiculous, but there is a point to it. As a codebase grows over years of use, there will be lots of opportunities to take a shortcut and just call a function that does only the work you think needs to be done. There might be a FullUpdate() function that calls PartialUpdateA(), and PartialUpdateB(), but in some particular case you may realize (or think) that you only need to do PartialUpdateB(), and you are being efficient by avoiding the other work. Lots and lots of bugs stem from this. Most bugs are a result of the execution state not being exactly what you think it is.
John Carmack:除了能知曉目前實際上哪些Code會被執行,inline函數還有個優點:他們不能在其他地方被呼叫。這聽起來似乎很荒謬,但這是有理由的。開發過程中Codebase會經歷數年的增刪修改,有很高的可能性有些人會圖方便呼叫某些小函數去完成他覺得那個函數會做的事。例如在main loop中可能有個FullUpdate()函數,其中呼叫PartialUpdateA(),PartialUpdateB()...有時候你會覺得我只需要做PartialUpdateB()就好,那我就直接Call PartialUpdateB()來做我想做的東西,這樣會比較有效率。但其實像這種事根本是Bug的根源。大部分的Bug都是因為目前的執行狀態並非如你所想。

大部分的Bug都是因為目前的執行狀態並非如你所想。
大部分的Bug都是因為目前的執行狀態並非如你所想。
大部分的Bug都是因為目前的執行狀態並非如你所想。

很重要所以要說三次。
當你把過多小函數獨立出來,你變相就在鼓勵這種Bug的發生。
而這些概念跟學校教我們的best practice是相反的,但很多程式語言的設計基本上是基於這些"best practice",如果我要做一個新的程式語言的話,這些並不是我想走的方向。

當然,像pure function的情形,這種情況factor out基本上是可以做的,雖然這樣做也會為程式引進新的概念,但至少他們不會造成許多bug。關於pure function我想以後再討論。

John Carmack在email後段寫到,我拿來總結以上想法:
I know there are some rules of thumb about not making functions larger than a page or two, but I specifically disagree with that now -- if a lot of operations are supposed to happen in a sequential fashion, their code should follow sequentially.
John Carmack: 我知道有些經驗法則說不要讓函數超過一兩個頁面,但我現在並不同意。如果有些操作本來就是循序發生,那這些操作所對應的Code就應該是循序的。

Locally-scoped Functions, Blocks and Codebase Evolution

如果程式語言中提供locally-scoped function的話倒是不錯的功能,因為local function不會造成某些問題:
1.我們不用擔心local function在其他地方被呼叫。
2.local function只跟local state有關。
因此local function是個不錯的想法。除此之外Local Block也有類似的效用,在C++中Local Block可以讓Code變得比較"衛生",他們可以防止該block中的變數去汙染到後續的code。另外Local block還有個好處就是他們提供一種方式讓你還不想factor out的相似操作可以暫時存在。
請看以下Code:
想像這段Code是一個大函數中的一部分,而且實際上我們不會只進行設定名子這種操作,請想像在同一個block中有很多其他操作在進行。一般來說,我們是以所謂"探索式"的方式在寫程式的,一開始其實大家還不確定最終成品會是什麼樣子,起初我們可能只是想實驗一些東西,在這種情況下其實是想到什麼就寫什麼,所以會有像上面一樣看似很糟糕的Code。像上述情形如果有local block的支援會讓我們方便許多,類似的操作可以獨立成一塊一塊的Block,變數不會互相干擾,而且這些區塊可以成為未來重構時函數的雛形,如下面這段Code:

當開發過了ㄧ段時間,我們已經大致確定要的是什麼東西之後,就可以把block中重複的部分重構成區域函數。上面這段Code中的lambda函數基本上就跟之前local block中的內容是一樣的。在C++中,目前必須用lambda來達成local function的功能。但據說目前C++的lambda有效能上的問題。據說實作上為了紀錄[]中capture的變數,會在heap上面allocate一個table來達成。
再過一段時間之後,有天你可能會想讓其他的函數也能呼叫這個maker,這時候我們終於可以將local function移到外面讓整個程式都看的到了,雖然我不確定這樣做是好是壞:
好,現在一定有人會問說:"為什麼不一開始就用最後這種寫法?"

最主要的理由是,現實世界是如此複雜,我們不是一開始就知道我們所面對的問題最佳解是什麼,尤其在遊戲這種程式環境中,我們甚至無法想像最種成品的樣貌,如果一開始就預設最後的解法會是怎樣而用上段Code的寫法來寫,以我過去的經驗是通常這些函數都必須重寫好幾次,與其用top-down的思考方式去寫程式,不如採用bottom-up的思維,我們一次實驗一些想法,慢慢趨近最佳解,讓問題本身來形塑他該有的解法。等開發了ㄧ段時間,你非常確定了你想要什麼東西之後,再來進行重構。

關於這種程式設計方式,Casey Muratori有篇不錯的文章,以The Witness中的UI系統為例說明Code重構的過程,大家可以參考看看。
http://mollyrocket.com/casey/stream_0019.html

註: Casey是Jonathan的朋友,除了The Witness外現在正進行Handmade Hero專案,台灣時間禮拜一到裡拜五每天正午12點在Twitch.tv上實況從無到有不用任何library/engine寫出一個跨平台2D遊戲。

註2: Casey的實況會有Q&A時間,Jonathan Blow有時候會上去搞笑,上次Jon問了一個問題:"What is Variable? Can Variable run javaScript?"    Casey: " No, javaScript is too high performance that Variable can't run it."  不論是問題還是回答都令人XD

Choosing a Syntax For Our New Language


為了支持前段所述的程式設計方式,我們的新語言必須要有新的與C/C++不同的語法,如你所見的,C/C++在這方面的支援並不好,像上一段我們把lambda函數外移成獨立函數時,函數的宣告語法改變了,這對常常需要移動Code的情況來說對生產力是有害的。

Sean's declaration syntax

在變數宣告部分,友人Sean的想法不錯:
我們用" :" 來宣告type,"="來表示assign。
如果想用type inference,就用第5行的寫法。
如果只是要assign值給已宣告的變數,就用最後一種寫法。
而第5行的寫法跟下面這個f: auto =1是一樣的。

不是說像這樣的宣告方式就是最好的方式,有些人可能會覺得Code中標點符號太多不太好,但我的重點是無論如何,宣告方式必須有一致且易於理解,只要符合這兩項特質,就是不錯的宣告語法。

那麼函數的宣告方式呢?
對於函數,我們希望宣告方式能:
1.方便在local/global之間剪貼移動。
2.方便在匿名/非匿名/lambda之間切換。
3.方便在class method與非method之間切換。(由於不確定class method是不是好東西,這次暫不討論這點)

(註:這邊筆者省略一段Jon舉的將C++中lambda改成一般函數的過程,這邊大家只要知道1.C++在lambda/函數的宣告方式上不一致 2.Code有像上一大段所述的演化過程 就行了。)

在C++之中,一般函數與lambda在宣告與type上都不一致:
lambda前面必須要有[] capture,用來表示這個lambda可以存取哪些非local變數,C++用這個可以達成類似closure的功能。

在Rust之中,lambda跟一般函數的宣告也有差異,lambda的傳入參數要用奇怪的pipe符號(|)包起來,而且函數本體不用加{}。:

不知道基於何種原因Rust會這樣設計語法,或許是因為實作或Parsing上的方便之類的。不過至少Rust在lambda的跟函數的type上是一樣的。

(註: 筆者猜因為lambda的起源是lambda calculus,數學上等式只能以一行expression來表達,不能用multiple statement,很多語言lambda body只能有expression,例如python。 只能寫一行的東西若還要{}包起來語法上反而冗長,所以才會有這樣的設計。)

What I Learned In Programming School: The Good Part

Anonymous Functions (Lambdas) Are Not Big Deal

學校教的內容也有好的部分,其中之一就是:"匿名函數(or lambda)不是什麼大不了的東西。他們只是沒有名子的函數。"這些匿名函數是設計來方便讓你用在"可以用完即丟"的情形。

匿名函數(or Lambda)本質上跟一般函數沒有差異,但現在很多程式語言的設計思維卻把兩者看成不一樣的東西,或是把支援Lambda當作了不起的feature,當你秉持這樣的想法去設計成式語言時,反而跟匿名函數或lambda原本的設計目的背道而馳。

這邊來看一個例子,在學校時我們教的是Scheme語言,這是一個Lisp的方言,有許多不同的實作。我不會建議大家在產業界使用Scheme,因為對我來說他有許多缺點,例如它不是static typing,語法有些怪異難讀,效能有疑問,最重要的理由是它有Garbage collection。然而Scheme作為教學用語言能夠讓你學習許多程式語言的原理,從這觀點看Scheme是相當不錯的。

在Scheme中要定義一個函數,你會用define去定義一個symbol,在此例中是square。如果你需要參數則在square後面加上一個符號,如此例中的x。後面的(* x x)則是function body。Scheme的運算是prefix,*號寫在前面(實際上"*"也是一個函數)。

當你要定義local symbol你必須用let,基本上let跟define沒什麼不同。要用lambda時,你必須加上lambda關鍵字,後面加上參數跟函數本體。行3的語意是:"定義一個symbol square,square代表lambda (x) (* x x)"。

初看你會覺得scheme在lambda跟一般函數的宣告語法上也有差異,但實際上呢,在scheme中,define只是一個語法糖,最終它會被展開成此例中行5的寫法,也就是函數其實就是有對應symbol(有名子)的lambda。當你比較行3跟行5的語法,除了let跟define不同外其餘是完全一致的。

會讓人覺得lambda跟一般函數不同的地方在於,lambda可以有closure。當你在lambda中用到外部scope的變數時,那個變數的lifetime會跟lambda黏在一起,那個變數在他原本的scope結束時不會從stack上pop掉,而會存在heap上,跟你的lambda包在一起同生共死,我們把lambda還有它的環境(也就是你用到的外部scope變數)叫做closure。

而很多程式語言基於以上原因把函數跟lambda當作不一樣的東西來處理,例如C++中為了讓lambda可以支援類似closure的功能,而且又為了效能問題(不想把外部scope變數存在heap上,因為fragmentation跟較高的cache miss rate)讓你必須用[] capture外部scope的變數到lambda函數的stack上...這種作法只是為了效能的最佳化,為此而造成函數與lambda語法與實作都不一致,為程式語言引進兩種不同概念造成Programmer的負擔,這樣是不好的。

所以在我們的新程式語言中,函數的宣告語法目前是如此設想的:

如行1,我們用跟之前變數宣告一樣的語法來宣告函數,":="表示同時給它type還有值。而在行3中,第三個參數的部分則是一個lambda,請注意這個lambda跟行1的函數本體完全一模一樣。有些人可能會覺得return type部分(-> float)有些冗長,但如果你想要的話我們可以用type inference推導出return type,可以少寫一點code,但我還不確定這樣是好是壞,因為return type可以當作函數的良好註解之一。

再來行7的部分,跟變數一樣,這邊我們只給它type。
行8,跟行1是一樣的東西。
行10,如果f已經被宣告type,這時你可以直接assign另一個現存的函數給它。
行11,同上,只是我們直接把函數本體寫出來。
行12,這是一個類似map用來處理array的函數,我們給它一個自訂函數讓它處理array中的元素。

總之,不管在哪個地方,使用函數的語法都是一致的。

有些人可能會認為這些只不過是語法上的小伎倆,但我必須告訴你過去幾年跟C++搏鬥下來,我發現語法是非常重要的東西,這關乎程式設計的舒適度,不良的語法設計會讓你士氣低落,會讓你覺得不管做什麼事好像都是在沼澤中前進一樣痛苦。就像我希望我的生命是有意義的,我希望我key進的每個字元都是有意義的!

Capture !

在C++中,capture是放在函數的type前面,而在我們提出的新語言中,capture放在block前面。

為何要放在block前?第一,capture並不是type的一部分,type的宣告最好就放在左邊,這樣不會破壞之前一直強調的語法一致性。如果把capture放在type前,萬一當你搞錯type時,你要修改type就沒那麼容易,因為你必須把capture也改掉,反之如果type是獨立在左邊的,你要修改的話就直接copy左邊的部分貼上就好了。

再來如果capture變成block的一種屬性的話,這似乎是個有趣的想法,我們可以讓每個block也都跟函數一樣有capture:
之前提到在Codebase的演化過程中,local block可以成為之後重構函數的雛形,但是local block本來就可以存取外部scope的變數,有必要用Capture嗎?如果有Capture,這個Capture可以限制這個local block可存取的外部scope變數,這樣可以防止block內的東西汙染到其他code,而且如此一來block更像函數了,這對未來的重構也是有幫助的,正如上段Code中,我們可以發現,block可以在local block->local block with capture->local lambda ->local or global named function四個階段之間輕鬆的漸變,且格式一致。

另外,Capture可能也對thread safety有幫助。通常一開始我們會用single-thread, synchronous的方式來寫Code並測試,之後再擴展成Multi-thread,而基本上測試時都會先確保single-thread情形下的正確性。當你擴展成multi-thread的時候,很多Thread可能會存取一堆共享或非共享的變數,改變整個程式的狀態。如果有Capture的話,我們至少可以在compile time時先確定,某段非同步的block裡可以存取哪些外部scope變數,如果某個block違反了capture,compiler可以先報錯。如此一來大大縮小了可能出錯的Code範圍。不過這邊只是我的猜想,還沒實作之前並不能驗證實用性。(註:筆者這邊省略一些code sample)

Capture也能用在Global function上,限制Global function所能存取的global state,這可以讓不純的函數更趨近於純函數。如下面這個pathfind函數:

像這個函數,我們在debug時可能需要存取一些Debug用的視覺化資訊,這時候如果把那些資訊放在capture內,可以確保這個global function不會碰到其他global變數,如果函數內用到其他global則compile time時報錯。

這邊用雙重[]是用來表示
"1.此函數中其他函數可以存取其他global跟
  2.只要在此函數中,任何函數呼叫都只能存取capture中的global "
中2.的行為。

像上述的capture設計,希望能開啟大家的討論空間。

Final Thoughts

Most of you have probably read various popular articles about the development process that produces the space shuttle software, and while some people might think that the world would be better if all software developers were that "careful", the truth is that we would be decades behind where we are now, with no PC's and no public internet if everything was developed at that snail's pace.
(筆者:最後這邊Jon講了一點Global的必要性跟引用John Carmack的信件做結尾,內容上跟之前講的差不多,我就連QA也一起省略了。)

(實作Demo請參考下一篇,To be continued...)



2014年11月11日 星期二

Jonathan Blow's Ideas About Programming Language雜記(1)





知名獨立遊戲開發者Jonathan Blow近日在twitch.tv上發表了一些有關"為了遊戲製作而生的程式語言"的想法,從9月中開始一共有3個talk,在第三個talk中還給出了他的prototype demo。而後來他還實況live coding了一些prototype feature,令我相當期待日後的發展。
而目前的3個talk,個人認為相當具有可看性,茲以此系列文筆記之。


Talk #1

Motivation

很多人將Jonathan Blow視為Game Designer,但實際上呢,他是Programmer出身,而就算他現在成立了一間遊戲公司,需要做許多商務工作,程式設計依然是他每天主要活動。
而他目前跟許多人一樣,每天最常使用的程式語言是C++,而C++依然是遊戲業界最普遍的語言之一。現在雖然有許多新的程式語言,但是大都缺乏C++的低階與高效能特性,例如指標與記憶體的直接操作,且C++的新規格C++0x/11/14等為C++加入了許多高階語言才有的功能,這些都讓C++在業界中保持良好的動能。

然而,Jon認為C++目前的複雜度已經高到難以接受的程度了,而且以他使用C++0X/14新特性的經驗,發現這些新特性要不是效能有問題,就是使用方式上跟過去的C++ feature有許多不一致的地方,這些都造成Game Programmer日常生活巨大的痛苦,現在是跳離C++這條大船的時候了。

因此,Jon開始了這一系列的Talk,他的目的不是要宣布一個新程式語言開發計畫,而是要開啟遊戲業界對這個問題的關注,並討論可能的改善方法。

Not a Big Agenda Language

Jon認為如果我們要轉換到另一種語言的話,它不能是一個Big Agenda Language。
什麼是Big Agenda?

"Functional EVERYWHERE." (Haskell中槍)
"Buffer overrun should be IMPOSSIBLE." (Rust?)
"EVERYTHING is an object." (Java,C#...)

如果程式語言採用極端且強迫的設計哲學,那有很高的機率會不夠實用,因為現實的世界充滿著各種意外與不一致性,若要強迫使用某一種思維方式去解決所有的問題,必定有極大的困難。

當然這些agenda基本上是好的,但是當我們回顧過去的歷史會發現許多agenda的實用性並未被證明,而在某些領域某些agenda可能永遠不適用。

Alternatives

現在又新又潮的程式語言百百種,我們又何必重新造輪子呢?
Jon認為目前有3個語言有可能是我們的替代方案: Go,D,Rust.但是這三種語言各自都有(對遊戲來說)不適用的理由。

Go:
Jon覺得Go作對了很多事情。但是因為
1.有Garbage Collection
對需要高效能的遊戲或遊戲引擎來說,GC是完全無法容忍的。或許現在有Unity這種東西,很多人也用它做出很多賺錢的遊戲,但是這些遊戲的Asset  loading通常不高,這就不在Jon所想討論的情形之中。
2.Concurrency Model對遊戲程式來說不適用。
Jon認為Go的Concurrency為了safety作了太多限制,而遊戲程式不需要限制那麼多。

D:
1.GC is optional. Good!
2.Buys too many into C++'s idea.
因為它的走向太像C++了,這是Jon所想避免的。
而目前如果要轉換到D的話,Jon認為並不值得,因為必須porting的部分太多,弊大於利。不過或許10年後情況改變了也不一定。

1.No GC.
2.但是它為了Safety作出過多限制,例如unsafe code必須包含在Unsafe block中,遊戲的Code到處充滿了side effect,如果每次寫之前都要考慮哪些safe哪些unsafe,把unsafe block用unsafe{}包起來,那對遊戲程式設計來說實在太痛苦。

所以上述三種程式語言通通出局XD

Feasibility

等等,但是造一個全新的程式語言,不會太工程浩大了嗎?

It's not that hard.

遊戲業界每年都會推出數款AAA級遊戲,這些遊戲程式每個都是不可思議的複雜,
Jon認為這些遊戲可能都比compiler複雜,如果我們把同等精力與資源投入改善工具,可以大大的改善整個業界的生產力。

更何況現在有LLVM這種東西。要知道做一個新的語言難處不是在Parsing或是Code gen,而是Optimization,而這部分LLVM可以幫助我們。

另外這是一個態度問題,想想當幾年前沒人看好電力車的情況下,Elon Musk都可以在美國建好他的電力車充電網,並讓Tesla公司營運上軌道,別說任何事情是不可能的。這個世界總是需要有人去push the envelope.

How Much Could We Gain?

Jon認為以我們現有的科技應該至少可以提升10% ~20%(相對於C的效能或生產力)
看起來好像不多,但是加上The Joy of Programming==無價。
所謂的The Joy of Programming是說:這個語言不會造成Coding上的摩擦力,你不會覺得要做什麼東西都綁手綁腳的,你會很容易進入FLOW狀態(零的領域?),如果這是一個你每天都要用的東西的話,那這點的價值是極大的。

Primary Goal

這個新的程式語言目標是:
Friction reduction
The joy of programming
Performance
Simplicity
Designed for good programmer

前兩項如前一段所述,總之就是這個程式語言必須讓程序員容易揮灑。而且有同等於C等級的機器效能或者人工效能(應該是指類似生產力之類的東西)。
另外這個語言必須要夠簡潔,這邊Jon拋出一個曲線圖來表示生產力隨著程式的複雜程度而下降。
再來,現在很多高階程式語言都是為平均或平均水準以下的programmer設計的,當你有這樣的想法的時候,整個設計哲學就會傾向讓average programmer減少犯錯而設下很多限制,這些限制可能反而造成高手揮灑困難。這就像如果你讓Chuck Yeager來當你的駕駛員你不會希望他去開客機吧?Yeager就是要開實驗戰鬥機阿!

最後則呼應了前面的"not a big agenda language",Jon認為實務上,程式語言只需要提供85% solution而不是100% solution。

RAII

接著,Jon發表了一些對C++裡的RAII特性的"個人意見":

RAII是什麼呢?原文是Resource Acquisition Is Initialization.
是一種由C++發展出來而後被用在許多OO程式語言裡的programming paradigm。
依照RAII原則,物件所持有的資源(resource)跟此物件的life-time同生共死。因此,C++裡有Constructor/Destructor,當物件創建時便在constructor中將其相關資源(例如Mesh file、Texture map等)一併初始化,當物件生命週期結束時便在Destructor中釋放其占據的資源。只要每個物件都遵循RAII原則,那對stack上的物件就不會發生Memory leak,更重要的是,C++中有Exception這種功能,如果物件都遵循RAII原則,那就算Exception發生,也不會發生Memory leak。當初RAII的發展就是為了Exception handling而生的一種配套。

這裡還有個問題:"什麼是資源(Resource)?"
一般來說,我們會認為像Memory,File handle,Texture map...等可以稱之為"資源"。但是仔細想想,這些東西本質上是相當不一樣的,在遊戲中我們對三者的操作方式通常是截然不同的,我們會對memory作new,delete,pooling,對File handle讀取檔案內容,parsing,然後關掉file。對Texture map呼叫OPENGL/D3D去改變render state。而RAII試圖強迫我們以同一種抽象去處理這些不一樣的"資源",帶來的效益並不大。Jon說在整個遊戲的程式碼中處理File或是texture map的code可能只占了1%,那就算沒有RAII,他也不覺得會怎樣。

但是呢,上面提到的三種資源中,有一個跟其他是非常不一樣的:"memory"。
甚至可以說我們程式的本質就是在操縱memory,操縱記憶體是我們90%的時間在做的事。所以,一個好的程式語言最好提供方便我們操縱記憶體的設計,而RAII不但沒有讓記憶體操作更方便,還綁手綁腳的規定我們必須要有constructer/destructor/copy constructer/move constructer/最好還來個"==" operator...etc.

RAII不過是為了解決Exceptionn所造成的問題,這邊Jon的用詞有點重:

Exception is silly.

他認為Exception是C++的feature中最複雜的功能之一。Exception破壞了程式的循序結構,在一段程式碼中,隨時都有可能某個物件拋出某個exception,造成程式skip掉下面整塊程式碼而跳到exception handling的Code,就像舊時代的Goto一樣危險且令程式難以理解。

現在C++社群中認為Exception是有害的並不在少數,
如D語言的發展者之一:
在這個Talk之中說明了大部分C++ Exception所產生的問題,並提出解決方案,雖然Jon認為其解決方案太Overkill。再者,Exception是程式語言發展的包袱,每當你提出一個新Feature,就會有人說:"這樣不行啦,會有Exception。" 程式語言的發展若考慮太多這些並無實質效益的東西,得不償失。而且對Programmer而言,這會造成巨大的阻力,對遊戲開發者來說,我們常常會需要快速實驗一些構想,如果我們連簡單的循序操作都需要把procedure包裝成物件、寫好Constructer,Destructor,copy constructer,考慮exception會如何發生...etc. 人的working memory實在有限,每次都要考慮code在哪種非local情形會跳到哪個block也實在太痛苦。

不是說exception完全不好,而是為了exception我們犧牲了太多。
像Go在這方面就做的不錯,在Go中可以有multiple return value,error code處理比起exception來簡單許多。

Helping with Memory

前面提到了好的程式語言最好提供方便記憶體操作的設計,這段Jon提出了幾種方式,這些方式並未經過驗證,只是Jon覺得可以朝這些方向去思考,大家可以一起來討論激盪出新的火花。

1.Memory ownership notation
首先提出的是以"!"符號代表memory ownership.請看以下幾段Code:

Jon說C++11之後允許這樣的初始方式,不使用Constructor,不需要header file,不會暴露type資訊到其他檔案,也減少了cross referencing,相當簡單好用。
這樣做的問題是:沒有Destructor,其中character_name跟class_name字串可能是allocate在heap上的,沒有destructor如何回收記憶體?

那麼這樣如何?
在char*後面加上"!"(用哪種符號並不重要),用來表示這個變數擁有這塊記憶體,如果這個變數生命週期結束或被呼叫了delete,就回收這塊空間。如此一來,連destructor都不用寫,簡單的syntax construct就能替代destructor的功用。

當然,這樣做可能遭遇"釋放已釋放記憶體"的bug。試想若有另一個該struct的copy,那當copy destruct的時候不就造成這種bug了嗎?
是的,的確是這樣,也有可能造成dereference已經釋放的記憶體區塊的問題,但是這些問題都可以藉由在程式語言spec中,規範compiler及Debugger必須在compile time或run time回報錯誤及錯誤發生的位置與行數,讓程序員自行解決。目前是有些3rd party工具在做類似的事,但是這些工具大都效能低落,而且因為compiler不知道memory ownership,無法標出造成錯誤的那段程式碼在哪裡,他們只能告訴你0xfeeefeee表示"釋放已釋放記憶體"這種狀況。

在Mesh class中也可以這樣用:



此時,C++粉絲冒出來了:"嘿嘿,這傢伙果然是個蠢蛋,unique_ptr不就是你要的嗎?"
確實unique_ptr解決這個問題,但是template compile很慢,錯誤訊息很難以理解。且站在程式碼美學的觀點來看,許多人會認為unique_ptr有些冗長,更糟的是程式碼的抽象概念改變了: 當我們宣告Vector3 *!時我們所要表達的重點是"Vector3",unique_ptr *他的type卻是"unique_ptr",Vector3這個"參數"似乎變成可有可無的東西。唯一性應該是type的一種參數,而非type是唯一性的一個參數,後者是一種奇怪的概念。


接下來這邊講range-checked array,而且在語法上與指標保持一致性,而非像C一樣放在後面。當然這種construct也需支援"!"。

前一個Struct Mesh不太現實,通常game programmer會像上面這樣做。因為Heap allocation很慢很貴,為了避免fragmentation與增加效能通常我們會將一起用到的東西allocate在同一塊連續的記憶體裡。這種寫法太常見了,但是老實說寫起來相當冗長麻煩且易錯。如果語言能提供一種設計讓人標示joint allocation,會非常方便,如下:
上面"@joint"這種markUp就是用來描述這樣的情形,如此一來compiler就能知道positions,indices與uvs必須allocate連在一起。如果compiler夠聰明的話,或許也能幫我們檢查
在保留記憶體空間時是否有一起保留,如最後三行code,如果其中一行沒有執行的話,compile time時就直接報錯。

以上這些Idea是比較主要的幾點,有些可能有點dumb或是trivial,但沒關係,這個talk目的本來就只是開啟後續的討論空間,有些觀點也只是個人經驗,並不是叫大家都相信他。

後面幾點是比較瑣碎的東西,有幾點在Talk #2 /#3有較詳細的討論,這邊我就簡單紀錄如下,詳細討論就留待後續文章。

No God Damn Header Files

Forward declaration,include guard,pragma once...這些東西最好不要出現。


Refactorability

程式語言的語法設計必須要考慮到編輯的方便性,我們在寫程式的時候是常常copy-n-paste的,如果語法沒有一致性,copy-n-paste時就要改很多東西。如C++裡的->跟'.',當變數是object時你會用後者,此時若copy那段code到另一個function裡若是參數改成傳指標的話,該段code的'.'就必須改成->了,當然C++中我們可以用reference,但是為了兩個本質上相同的東西引進兩種不同概念是不必要的。據說Go在這點上做得很好。

另外C++中function pointer雛形宣告方法太多樣,缺乏一致性,lambda的語法跟一般函數宣告也不相同,這些都造成refactor時的阻力。

這邊在Talk #2中著墨許多,若有興趣請看後面文章。

Concurrency

這邊講到C++ lambda的[] capture對concurrency造成的效應。Jon說這邊他還沒有想很多,實際上在Talk #2後段是講到蠻多的,所以就同前段最後一句。

The program specifies how to build it.

這個特色呢則是Jon認為非常重要而且可改進的地方。
一個程式必須說明如何Build自己。語言的spec規定compiler必須做完所有的事(compiling,linking...),如此一來你在不同的OS平台都只需要一個compiler跟你的source code,而不需要像現在在Windows上用VC,在Linux上用gcc/g++/clang,也不需要像CMAKE這種cross-build工具的存在。語言的spec必須解決這些問題。

這點在Talk #3 Demo中有相當有趣且強大的展示,有興趣者可以參考影片。

No implicit type conversions
Named argument passing
Less mark-up
Permissive License
Optional Type
No preprocessor or better prepocessor

(以上及Q&A省略)
(To be continued in next article.)