原文。
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ù)添加了一個構造器。我們會在其中存儲以下四種信息:
- 與函數(shù)體綁定的參數(shù)名稱;
- 函數(shù)是否接受可變長度的參數(shù),如果接受的話,參數(shù)綁定的變量是什么;
- 一個表達式列表,也就是函數(shù)體;
- 函數(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