Re: Build Your Own Haskell Compiler #0.4
遞迴讀檔
想要知道自己這個
Module
叫什麼名字的話,看ModuleHead
,想要看 import 出些什麼的話,看[ImportDecl]
。在上面還推薦大家要看的一個 library 叫做 Data.Map.Strict 。這個是我用來記錄
Module
到底讀過了沒有。你也可以用List
,但這個效率會比較好一點。只是用起來比較複雜一點。
在遞迴讀檔前,花了不少時間整理思路,最後的想法是:
- 我需要一個
Map
來蒐集 module name 與 module AST 的對應關係。 - 我希望得到的結果在
IO
裡面,因為readFile
會給我IO
,我也需要IO
才能 pretty print 。 - 我希望可以遞迴地處理所有 import 進來的檔案。
於是 type 長這樣:
collectModule :: IO (M.Map String (Module SrcSpanInfo)) -> Module SrcSpanInfo -> IO (M.Map String (Module SrcSpanInfo))
程式一開始先處理 Module
:
collectModule ioMap mod =
case mod of
Module _ mModuleHead _ imports _ -> -- 略
_ -> ioMap
然後問問 Map 裡面是不是已經有現在要加入的 Module
了:
collectModule ioMap mod =
case mod of
Module _ mModuleHead _ imports _ -> do
let
modName = case mModuleHead of
Just (ModuleHead _ (ModuleName _ name)) -> name
Nothing -> "Main"
map' <- ioMap
let
map'' = case M.member modName map' of
False -> M.insert modName mod map'
True -> map'
-- 略
_ -> ioMap
在那個 do
之後就是 IO
的領域了,於是 let
後面不用 in
, map'
則是把 type 是 IO (M.Map String (Module SrcSpanInfo))
的 ioMap
裡的 (M.Map String (Module SrcSpanInfo))
拿出來。
奇怪的是那兩個 let
與 case ... of
的組合,如果寫:
let modName = case mModuleHead of
Just (ModuleHead _ (ModuleName _ name)) -> name
Nothing -> "Main"
是會得到 parsing error 的,但是寫:
let modName = case mModuleHead of Just (ModuleHead _ (ModuleName _ name)) -> name
Nothing -> "Main"
會過,像前文那樣寫也會過。覺得前文那種寫法比較美觀,故沒有把 let
和 case ... of
寫在一起。
最後則是讀檔,再遞迴地呼叫自己:
collectModule ioMap mod =
case mod of
Module _ mModuleHead _ imports _ -> do
let
modName = case mModuleHead of
Just (ModuleHead _ (ModuleName _ name)) -> name
Nothing -> "Main"
map' <- ioMap
let
map'' = case M.member modName map' of
False -> M.insert modName mod map'
True -> map'
go acc [] = acc
go acc (m : ms) = go modMap ms where
modMap = do
let (ModuleName _ name) = importModule m
case name of
"Prelude" -> acc
_ -> do
mMod <- readModule name
case mMod of
Just mm -> collectModule acc mm
Nothing -> acc
go (return map'') imports
_ -> ioMap
go
的 type 其實是:
go :: IO (M.Map String (Module SrcSpanInfo)) -> [ImportDecl] -> IO (M.Map String (Module SrcSpanInfo))
當 go
吃到的 [ImportDecl]
空了([]
)時,就把 acc
原封不動地還回去。
當 [ImportDecl]
中還有東西時,就先把裡面第一個東西處理完,放到 modMap
中,再把剩下的交給 go 繼續處理。而 modMap
是什麼呢?是先看看這第一個叫 m
的 ImportModule
的名字,如果不是預設會自動 import 的 Prelude
的話,才交給 readModule
把 module 讀進來,看看有沒有讀成功(是 Just mm
還是 Nothing
),成功就交給 collectModule acc mm
遞迴處理,失敗就傳回本來的 acc
。不用先把 acc
這個 IO (M.Map ...)
裡的 Map
拆出來,那在下一次遞迴時會被 collectModule
處理好。
最後 go (return map'') imports
之所以要加上 return
,是為了把沒被放在 IO
裡的 map''
放進去,這樣才能交給 go
處理。
然後 main
只要這樣寫:
main :: IO ()
main = do
inputStr <- getContents
let res = parseModule inputStr
allMods <- case res of
ParseOk mod -> collectModule (return M.empty) mod
ParseFailed _ msg -> return M.empty
mapM_ putStrLn $ fmap prettyPrint allMods
最後那行 mapM_ putStrLn $ fmap prettyPrint allMods
可以讀作:把 prettyPrint
套到所有 modules 上面,然後一個一個印(putStrLn
)出來, mapM_
最後會還給我們 IO ()
,功德圓滿。
只剩處理檔案路徑,就完成這次的進度了。