caasih.net

Re: Build Your Own Haskell Compiler #0.4

遞迴讀檔

想要知道自己這個 Module 叫什麼名字的話,看 ModuleHead ,想要看 import 出些什麼的話,看 [ImportDecl]

在上面還推薦大家要看的一個 library 叫做 Data.Map.Strict 。這個是我用來記錄 Module 到底讀過了沒有。你也可以用 List ,但這個效率會比較好一點。只是用起來比較複雜一點。

在遞迴讀檔前,花了不少時間整理思路,最後的想法是:

  1. 我需要一個 Map 來蒐集 module name 與 module AST 的對應關係。
  2. 我希望得到的結果在 IO 裡面,因為 readFile 會給我 IO ,我也需要 IO 才能 pretty print 。
  3. 我希望可以遞迴地處理所有 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 後面不用 inmap' 則是把 type 是 IO (M.Map String (Module SrcSpanInfo))ioMap 裡的 (M.Map String (Module SrcSpanInfo)) 拿出來。

奇怪的是那兩個 letcase ... 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"

會過,像前文那樣寫也會過。覺得前文那種寫法比較美觀,故沒有把 letcase ... 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 是什麼呢?是先看看這第一個叫 mImportModule 的名字,如果不是預設會自動 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 () ,功德圓滿。

只剩處理檔案路徑,就完成這次的進度了。

Isaac Huang 發佈於