Write Yourself a Scheme in 48 Hours/Defining Scheme Functions

原文。
https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours/Defining_Scheme_Functions

現(xiàn)在既然可以定義變量了,我們就來把它擴展到函數(shù)上來。在這章之后,你就能夠在你的Scheme里定義并使用你自己的函數(shù)了。我們的整個實現(xiàn)也就基本完成了。

讓我們從給LispVal定義新的構造器開始:

| PrimitiveFunc ([LispVal] -> ThrowsError LispVal)
| Func { params :: [String], vararg :: (Maybe String),
         body :: [LispVal], closure :: Env }

我們?yōu)樵瘮?shù)添加了一個額外的構造器,因為我們會希望能夠將+,eqv?這樣的原生函數(shù)作為變量傳遞給其他函數(shù)。我們的PrimitiveFunc構造器包含了一個讀入?yún)?shù)列表然后返回一個ThrowsError LispVal的函數(shù),就和我們在primitive列表里存儲的類型一樣。

我們還為用戶定義的函數(shù)添加了一個構造器。我們會在其中存儲以下四種信息:

  1. 與函數(shù)體綁定的參數(shù)名稱;
  2. 函數(shù)是否接受可變長度的參數(shù),如果接受的話,參數(shù)綁定的變量是什么;
  3. 一個表達式列表,也就是函數(shù)體;
  4. 函數(shù)定義所在的環(huán)境。

這是一個record類型的例子。Record在Haskell中看起來有點笨重,因此我們也只是在這里示范以下。然而在大規(guī)模的編程開發(fā)中,他有著無可替代的價值。

接下來,我們在show函數(shù)中添加新的類型:

showVal (PrimitiveFunc _) = "<primitive>"
showVal (Func {params = args, vararg = varargs, body = body, closure = env}) =
   "(lambda (" ++ unwords (map show args) ++
      (case varargs of
         Nothing -> ""
         Just arg -> " . " ++ arg) ++ ") ...)"

我們這里對原生函數(shù)僅僅打印了<primitive>,對用戶自定義的函數(shù)則是打印出來頭部信息,而不是將整個函數(shù)體全部打印出來。這是一個對Record進行模式匹配的例子:與普通的代數(shù)類型一樣,模式看起來和構造器是一樣的。前面是字段名然后緊跟著的是會與值綁定的變量名稱。

接下來,我們需要修改apply函數(shù)。和之前傳遞函數(shù)名不同的是,現(xiàn)在我們直接將代表函數(shù)的LispVal值傳遞給它。對于原生函數(shù)來說代碼變得更簡單了:我們將函數(shù)值從參數(shù)中讀出然后應用就可以了。

apply :: LispVal -> [LispVal] -> IOThrowsError LispVal
apply (PrimitiveFunc func) args = liftThrows $ func args

當我們處理用戶自定義函數(shù)的時候,有趣的事情發(fā)生了。Record類型不僅允許你對字段名進行匹配,你也可以通過位置來識別它們,我們來試試看:

apply (Func params varargs body closure) args =
      if num params /= num args && varargs == Nothing
         then throwError $ NumArgs (num params) args
         else (liftIO $ bindVars closure $ zip params args) >>= bindVarArgs varargs >>= evalBody
      where remainingArgs = drop (length params) args
            num = toInteger . length
            evalBody env = liftM last $ mapM (eval env) body
            bindVarArgs arg env = case arg of
                Just argName -> liftIO $ bindVars env [(argName, List $ remainingArgs)]
                Nothing -> return env

這里第一步是確認參數(shù)列表的長度,判斷和期望的參數(shù)是否一致。如果不一致的話則會拋出一個錯誤。我們還定義了一個局部的num函數(shù)來增加代碼的可讀性并讓程序更短。

如果調用是合法的,那我們就會在Monad管理進行一系列操作,將參數(shù)綁定給新的環(huán)境,然后執(zhí)行函數(shù)體中的語句。我們做的第一件事就是將參數(shù)名稱的列表和已經(jīng)經(jīng)過計算的參數(shù)值列表通過zip函數(shù)拉成一個鍵值對的列表。然后我們用這個列表和函數(shù)的閉包(其實這并不是當前的環(huán)境,而只是函數(shù)的靜態(tài)作用域)組成一個新的環(huán)境并且將函數(shù)在其中進行求值。返回的結果是IO類型的,而整個函數(shù)的返回值是IOThrowsError類型,因此我們需要使用liftIO來將它進行轉換。

接下來,我們將剩余的參數(shù)通過局部函數(shù)bindVarArgs綁定給varArgs變量。如果函數(shù)不需要可變參數(shù)(Nothing子句),那我們就將現(xiàn)在的環(huán)境返回。不然的話,我們創(chuàng)建一個將變量名作為鍵,輸入?yún)?shù)為值的列表然后把它傳給bindVars。方便起見我們定義它為局部變量remainingArgs,并用內置的drop函數(shù)來忽略之前已經(jīng)綁定過得參數(shù)。

最后一步是在新的環(huán)境中對函數(shù)體進行求值。我們?yōu)榱诉@個定義了一個局部函數(shù)evalBody。它將eval env這個Monad函數(shù)映射到了每一個函數(shù)體中的語句,然后講最后一個語句的值返回。

我們現(xiàn)在將原生函數(shù)存儲在普通的變量值里,讓我們來在程序開始的時候預先綁定它們:

primitiveBindings :: IO Env
primitiveBindings = nullEnv >>= (flip bindVars $ map makePrimitiveFunc primitives)
     where makePrimitiveFunc (var, func) = (var, PrimitiveFunc func)

這里我們首先將最初的空環(huán)境讀入,將封裝好的原生函數(shù)扎成一捆鍵值對,然后再將它們一起綁定成新的環(huán)境。讓我們在runOne和runRepl里也替換成primitiveBindings函數(shù):

runOne :: String -> IO ()
runOne expr = primitiveBindings >>= flip evalAndPrint expr

runRepl :: IO ()
runRepl = primitiveBindings >>= until_ (== "quit") (readPrompt "Lisp>>> ") . evalAndPrint

最后讓我們來修改求值器讓它來支持lambda函數(shù)以及define功能。我們從幾個能在IOThrowsError中幫助我們創(chuàng)建函數(shù)對象的輔助函數(shù)開始:

makeFunc varargs env params body = return $ Func (map showVal params) varargs body env
makeNormalFunc = makeFunc Nothing
makeVarArgs = makeFunc . Just . showVal

這里makeNormalFunc和makeVarArgs函數(shù)只是MakeFunc函數(shù)的在普通情況和可變參數(shù)情況下的特殊形式而已。這是一個如何將函數(shù)看做一等公民然后簡化代碼的很好的例子。

現(xiàn)在我們用它們來添加新的求值子句。我們在定義變量以及函數(shù)應用的子句之間添加以下內容:

eval env (List (Atom "define" : List (Atom var : params) : body)) =
     makeNormalFunc env params body >>= defineVar env var
eval env (List (Atom "define" : DottedList (Atom var : params) varargs : body)) =
     makeVarArgs varargs env params body >>= defineVar env var
eval env (List (Atom "lambda" : List params : body)) =
     makeNormalFunc env params body
eval env (List (Atom "lambda" : DottedList params varargs : body)) =
     makeVarArgs varargs env params body
eval env (List (Atom "lambda" : varargs@(Atom _) : body)) =
     makeVarArgs varargs env [] body

之前的求值函數(shù)中的函數(shù)應用部分的子句也需要替換掉:

eval env (List (function : args)) = do
     func <- eval env function
     argVals <- mapM (eval env) args
     apply func argVals

正如你所見,這里我們用模式匹配來對輸入?yún)?shù)進行解構,然后調用適當?shù)妮o助函數(shù)。在定義define的時候,我們還需要將結果傳入到defineVar函數(shù)來將變量綁定到本地環(huán)境當中。我們還需要將函數(shù)應用部分的子句進行修改,因為現(xiàn)在apply函數(shù)能夠在IOThrowsError Monad中工作了,所以我們也不需要liftThrows函數(shù)了。

編譯并且運行程序,現(xiàn)在我們可以用它來寫我們自己的程序了!

$ ghc -package parsec -fglasgow-exts -o lisp [../code/listing9.hs listing9.hs]
$ ./lisp
Lisp>>> (define (f x y) (+ x y))
(lambda ("x" "y") ...)
Lisp>>> (f 1 2)
3
Lisp>>> (f 1 2 3)
Expected 2 args; found values 1 2 3
Lisp>>> (f 1)
Expected 2 args; found values 1
Lisp>>> (define (factorial x) (if (= x 1) 1 (* x (factorial (- x 1)))))
(lambda ("x") ...)
Lisp>>> (factorial 10)
3628800
Lisp>>> (define (counter inc) (lambda (x) (set! inc (+ x inc)) inc))
(lambda ("inc") ...)
Lisp>>> (define my-count (counter 5))
(lambda ("x") ...)
Lisp>>> (my-count 3)
8
Lisp>>> (my-count 6)
14
Lisp>>> (my-count 5)
19
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容