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)在既然可以定義變量了,我們就來把它擴(kuò)展到函數(shù)上來坛芽。在這章之后留储,你就能夠在你的Scheme里定義并使用你自己的函數(shù)了。我們的整個實現(xiàn)也就基本完成了咙轩。

讓我們從給LispVal定義新的構(gòu)造器開始:

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

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

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

  1. 與函數(shù)體綁定的參數(shù)名稱钾菊;
  2. 函數(shù)是否接受可變長度的參數(shù)尤误,如果接受的話,參數(shù)綁定的變量是什么结缚;
  3. 一個表達(dá)式列表损晤,也就是函數(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進(jìn)行模式匹配的例子:與普通的代數(shù)類型一樣篇裁,模式看起來和構(gòu)造器是一樣的。前面是字段名然后緊跟著的是會與值綁定的變量名稱赡若。

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

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

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

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

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

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

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

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

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

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ù)應(yīng)用的子句之間添加以下內(nèi)容:

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ù)應(yīng)用部分的子句也需要替換掉:

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

正如你所見,這里我們用模式匹配來對輸入?yún)?shù)進(jìn)行解構(gòu)姚淆,然后調(diào)用適當(dāng)?shù)妮o助函數(shù)孕蝉。在定義define的時候,我們還需要將結(jié)果傳入到defineVar函數(shù)來將變量綁定到本地環(huán)境當(dāng)中腌逢。我們還需要將函數(shù)應(yīng)用部分的子句進(jìn)行修改降淮,因為現(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末媒惕,一起剝皮案震驚了整個濱河市系吩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妒蔚,老刑警劉巖穿挨,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肴盏,居然都是意外死亡科盛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門菜皂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贞绵,“玉大人,你說我怎么就攤上這事恍飘≌ケ溃” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵常侣,是天一觀的道長蜡饵。 經(jīng)常有香客問我,道長胳施,這世上最難降的妖魔是什么溯祸? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮舞肆,結(jié)果婚禮上焦辅,老公的妹妹穿的比我還像新娘。我一直安慰自己椿胯,他們只是感情好筷登,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哩盲,像睡著了一般前方。 火紅的嫁衣襯著肌膚如雪狈醉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天惠险,我揣著相機(jī)與錄音苗傅,去河邊找鬼。 笑死班巩,一個胖子當(dāng)著我的面吹牛渣慕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抱慌,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼逊桦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抑进?” 一聲冷哼從身側(cè)響起强经,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎单匣,沒想到半個月后夕凝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡户秤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年码秉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸡号。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡转砖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鲸伴,到底是詐尸還是另有隱情府蔗,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布汞窗,位于F島的核電站姓赤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仲吏。R本人自食惡果不足惜不铆,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望裹唆。 院中可真熱鬧誓斥,春花似錦、人聲如沸许帐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽成畦。三九已至距芬,卻和暖如春涝开,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔑穴。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工忠寻, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留惧浴,地道東北人存和。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像衷旅,于是被迫代替她去往敵國和親捐腿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容