使用Zig來學(xué)習(xí)編譯原理——第一章習(xí)題參考答案
之所以是參考答案是因?yàn)槲也]有找到任何形式的官方答案,同時(shí)可能我對(duì)于習(xí)題的理解并不正確,導(dǎo)致實(shí)現(xiàn)的代碼和題目要求的不一樣也說不定,最主要的是函數(shù)式編程風(fēng)格我很不擅長(zhǎng)
首先,由于時(shí)間間隔實(shí)在太久了,zig的發(fā)生了較大的變化,原項(xiàng)目不做任何修改顯然是無法通過編譯了,我們不妨重新生成一個(gè)zig項(xiàng)目,然后復(fù)制原先的代碼(0.11在build.zig上做了較大改動(dòng))
然后我們來慢慢的修改代碼(當(dāng)然不修改目前版本也依舊可以運(yùn)行)
先版本的zig可以直接使用union(enum)來創(chuàng)建tagged union而無需專門為union創(chuàng)建一個(gè)成員相同的enum,另外這里稍微修改了一下其他的地方的一些細(xì)節(jié),同時(shí)上一期的maxargs的一種實(shí)現(xiàn)已經(jīng)包含在修改的代碼中了(畢竟比較簡(jiǎn)單)
至于解釋器的代碼,我們來簡(jiǎn)單的分析一下,首先,由于原文有要求需要按照函數(shù)式編程的理念來進(jìn)行(其實(shí)函數(shù)式的話,可能更適合使用Haskell或者ocaml,畢竟這些語言的默認(rèn)范式是函數(shù)式編程),所以這里我們會(huì)使用那個(gè)似是而非的鏈表(即原文提到的那個(gè)table類型,其實(shí)吧都3202年了,我們完全可以使用一些方便點(diǎn)的數(shù)據(jù)結(jié)構(gòu),這里就暫時(shí)按照原理來操作,之后如果有優(yōu)化的必要再說吧)
那么,最新版本的zig下的table的代碼其實(shí)并沒有什么變化(主要是將其構(gòu)造函數(shù)改為了返回所有權(quán)而非具有所有權(quán)的指針,其差別在于是否可以安全的賦值給某個(gè)指針):
按照原文的要求,我們需要一個(gè)用于解釋程序的函數(shù),該函數(shù)接收一個(gè)成員的根節(jié)點(diǎn)(暫時(shí)先這么稱呼吧),解釋并執(zhí)行相關(guān)的語句和表達(dá)式
同時(shí),根據(jù)原文對(duì)于函數(shù)式的要求,我們需要在這函數(shù)內(nèi)部創(chuàng)建table(一下內(nèi)容涉及指針,解引用等功能,代碼未必安全,可能會(huì)碰到一些各位當(dāng)年學(xué)c的時(shí)候的老朋友,例如segmentation fault之類的,但由于zig本身一定的保證安全性的方案,故而錯(cuò)誤不會(huì)那么明顯):
首先是這個(gè)作為入口的函數(shù),初始化一個(gè)空的鏈表頭節(jié)點(diǎn)(主要該節(jié)點(diǎn)為空,不代表不需要分配內(nèi)存給它,有些人可能會(huì)直接undefined,但后面在遍歷鏈表的時(shí)候需要不斷的對(duì)tail進(jìn)行解引用,而最開始的節(jié)點(diǎn)會(huì)在最后,如果它是undefined則會(huì)因此崩潰,當(dāng)然你也可以在后續(xù)第一次使用到的時(shí)候再初始化,但這貌似違背了函數(shù)式編程的規(guī)矩)
然后是stm和exp的兩個(gè)解析函數(shù)(稍微加了一點(diǎn)調(diào)試用的信息):
stm的解析函數(shù)
exp的解析函數(shù):
這里就不展開講解代碼了,主要需要注意的就是zig的指針默認(rèn)都是*const,故而不能被另一個(gè)指針賦值,只能通過解引用賦值,這樣雖然可以解決部分垂懸引用的問題,但會(huì)將一部分問題轉(zhuǎn)化為內(nèi)存泄漏問題,由于zig的字符串不能直接比較,所以這里用了zig的cstr來比較字符串(其實(shí)可以使用內(nèi)存操作來比較),此外,可以看到除法這里調(diào)用了一個(gè)內(nèi)置函數(shù),這不是在下特立獨(dú)行,而是當(dāng)前版本的zig追加了新的規(guī)定,即兩個(gè)整數(shù)類型不能直接使用/進(jìn)行除法,只能在四個(gè)內(nèi)置的除法函數(shù)中選擇一個(gè)進(jìn)行調(diào)用,具體的可以查看zig的官方文檔,這里將絕大部分的錯(cuò)誤處理都寫成了catch unreachable,主要是這些錯(cuò)誤都是內(nèi)存錯(cuò)誤,本就是會(huì)導(dǎo)致程序崩潰的,后續(xù)處理并不是十分重要,如果需要的話,將unreachable替換為@panic("xxx")即可
至于課后練習(xí)的問題,由于當(dāng)前版本很多針對(duì)指針的功能受到了默認(rèn)不可變指針的影響導(dǎo)致現(xiàn)在很多使用指針的地方需要考慮將不可變指針轉(zhuǎn)換為可變指針,此時(shí)使用一層Option類型套在Node外面別說是新手了,我寫了這么久代碼,我都很頭疼你可能會(huì)碰到億點(diǎn)點(diǎn)的expect type *xxx, found *const xxx,如果此時(shí)調(diào)用內(nèi)部函數(shù)還可能返回error union的話,簡(jiǎn)直是地獄,代碼可讀性斷崖式下跌,所以這里我們不妨使用zig內(nèi)置的optional類型來寫這個(gè)二叉搜索樹。
那么,此時(shí)該二叉搜索樹大概長(zhǎng)這樣:
member函數(shù)可能長(zhǎng)這樣(這里我們的樹存儲(chǔ)了一個(gè)值,所以查找函數(shù)也將返回一個(gè)值,而非簡(jiǎn)單的true或者false)
編寫測(cè)試驗(yàn)證代碼是否正確:

實(shí)際上在下對(duì)于題目的理解可能有誤,member這里也可能是查找的某一個(gè)節(jié)點(diǎn)是不是樹的一員,由于我們一開始的實(shí)現(xiàn)就是包含值的,所以這里我們實(shí)現(xiàn)的member其實(shí)就是第二個(gè)問題要求的lookup,而insert一開始就是符合第二題要求的
第三個(gè)問題其實(shí)很簡(jiǎn)單,這里在下甚至都不需要具體的例子來說明,簡(jiǎn)單的描述就是,但我們構(gòu)建一個(gè)二叉搜索樹的時(shí)候,我們希望它盡可能的是一個(gè)樹形結(jié)構(gòu),但有時(shí)候如果一組數(shù)據(jù)非常規(guī)整,甚至特殊情況下就是拍好順序的,此時(shí)你可能會(huì)得到一個(gè)鏈表,此時(shí)時(shí)間復(fù)雜度上升,資源占用還多余鏈表,此時(shí)就需要我們?cè)跇?gòu)建二叉搜索樹的時(shí)候盡量讓其保持樹形,即保持其平衡性
然后最后一個(gè)問題,為其實(shí)現(xiàn)平衡性,一種立刻就能想到的辦法就是記錄當(dāng)前的深度和節(jié)點(diǎn)數(shù)量,以此判斷該樹是否平衡,并以此來進(jìn)行自動(dòng)排序,也就是題干中所說的自調(diào)整樹,但該實(shí)現(xiàn)非常OOP,不符合函數(shù)式規(guī)范,故而被出題者禁止了。但同時(shí)出題人提出了應(yīng)當(dāng)在插入時(shí)保持平衡,那么我們也可以很快想到一種實(shí)現(xiàn),即根據(jù)左右子樹的深度判斷是否需要重排,然后分別實(shí)現(xiàn)左右兩種旋轉(zhuǎn)函數(shù)
那么統(tǒng)計(jì)高度的函數(shù):
左右選擇的函數(shù):
修改后的insert函數(shù):
本來打算習(xí)題和下一章一起更新的,但僅僅習(xí)題就花費(fèi)了如此長(zhǎng)的篇幅,那么就分開更新吧