原子词素解析器
parseAtom :: Parser LispVal
parseAtom = do first <- letter <|> symbol -- 字母或操作符开头
rest <- many (letter <|> digit <|> symbol) -- 一到多个
let atom = [first] ++ rest -- 迭加
return $ case atom of -- 根据词素解析生成返回的解析值
"#t" -> Bool True
"#f" -> Bool False
otherwise -> Atom atom
练习 2.3.2
(Number . read) 如果“.”两边没有空格会出错,应该是错把 Number.read 当 作一个类型了。 看了这个才知道我对 Monad 还是玩的不熟,意识不够:
parseNumber :: Parser LispVal
parseNumber = many1 digit >>= (return . Number . read)
练习 2.3.3
这个逃逸字符处理的思路我想到了,但是还是没有意识到 do 模式下 many 和 return 可以这样简洁漂亮的组合:
escapedChars :: Parser Char
escapedChars = do char '\\'
x <- oneOf "\\\"nrt"
return $ case x of
'\\' -> x
'"' -> x
'n' -> '\n'
'r' -> '\r'
't' -> '\t'
附扩展后的 parseString ,这个倒没大变化:
parseString :: Parser LispVal
parseString = do char '"'
x <- many $ escapedChars <|> noneOf "\"\\"
char '"'
return $ String x
练习 2.3.4
基本上进制不同的解析是一个力气活。我原本想用 do return case 的方式,但 是后来看还是答案中的每种进制分别解析比较合理,因为逻辑上要区分一些特殊情况。
这其中需要注意的知识点就是 Numeric 模块中的 readXXX 函数都是返回二元组, 要像如下代码这样取第一个元素出来才是我们需要的答案。
oct2dig x = fst $ readOct x !! 0
hex2dig x = fst $ readHex x !! 0
bin2dig = bin2dig' 0
bin2dig' digint "" = digint
bin2dig' digint (x:xs) = let old = 2 * digint + (if x == '0' then 0 else 1) in
bin2dig' old xs
练习 2.3.5
这里在函数内部嵌套 do 的技巧值得学习
parseCharacter :: Parser LispVal
parseCharacter = do
try $ string "#\\"
value <- try (string "newline" <|> string "space")
<|> do {x <- anyChar; notFollowedBy alphaNum; return [x]}
return $ Character $ case value of
"space" -> ' '
"newline" -> '\n'
otherwise -> (value !! 0)
这里开始更多的使用 try ,这个函数我还不熟悉。
parseExpr :: Parser LispVal
parseExpr = parseAtom
<|> parseString
<|> try parseNumber
<|> try parseBool
<|> try parseCharacter
练习 2.3.7
原文这里似乎有错,我最终用的是 parseNumber 而不是 parseDecimal,这是考 虑到应该允许输入不同进制的复数。
parseComplex :: Parser LispVal
parseComplex = do x <- (try parseFloat <|> parseDecimal)
char '+'
y <- (try parseFloat <|> parseDecimal)
char 'i'
return $ Complex (toDouble x :+ toDouble y)
3.4 Recursive Parsers
CLOSED: 2010-11-23 二 01:00 递归解释器这里令人赞叹的利用了 monad then : CLOSED: 2010-11-23 二 01:00
parseDottedList :: Parser LispVal
parseDottedList = do
head <- endBy parseExpr spaces
tail <- char '.' >> spaces >> parseExpr
return $ DottedList head tail
有前趋状态(如某些数据会引用前缀,类似数组则会迭加元素)的时候,需要用 try 。可以确保出错时回溯到出错前的状态。
在加入了练习补充内容的代码中增加 list 解析过程时,会有编译错误,需要 把 list 的解析器单独取出来。
parseLst :: Parser LispVal
parseLst = do char '('
x <- (try parseList) <|> parseDottedList
char ')'
return x
然后 parseExpr 就成为这样:
parseExpr :: Parser LispVal
parseExpr = parseAtom
<|> parseString
<|> try parseNumber
<|> try parseRatio
<|> try parseFloat
<|> try parseComplex
<|> try parseBool
<|> try parseCharacter
<|> parseQuoted
<|> parseLst
在本节的几个解析器实现中,递归调用 parseExpr ,实现了整个解析器对递归语 法结构的解析。
练习 2.4.3
在这里 wikibooks 给出了两组不同的实现,一个是实现一种 AnyList。此实现有 一些错误,首先是没有给出 Nil 的实现,我自己尝试做了一个:
data LispVal = Atom String
| List [LispVal]
| DottedList [LispVal] LispVal
| Number Integer
| Float Double
| String String
| Bool Bool
| Character Char
| Ratio Rational
| Complex (Complex Double)
| Vector (Array Int LispVal)
| Nil ()
然后 optionalSpaces 也不存在,实际上应该就是 spaces。
parseAnyList :: Parser LispVal
parseAnyList = do
char '('
spaces
head <- sepEndBy parseExpr spaces
tail <- (char '.' >> spaces >> parseExpr) <|> return (Nil ())
spaces
char ')'
return $case tail of
(Nil ()) -> List head
otherwise -> DottedList head tail
该页上还给出一种方法是扩展 parseList ,此方法不使用 Nil。
-- parseList' :: Parser LispVal
parseList' head = do char '.' >> spaces1
tail <- parseExpr
spaces >> char ')'
return $ DottedList head tail
parseList :: Parser LispVal
parseList = do char '(' >> spaces
head <- parseExpr `sepEndBy` spaces1
(parseList' head)<|> (spaces >> char ')' >> (return $ List head))
此例中仍然遇到在有 choice 运算符(< | >) 的情况下无法嵌套 do,于是定义了一个辅助函数。 |
Evalution, Part 1
4.2 Beginnings of an evaluator: Primitives
因为 Lisp 族系代码即数据,所以 eval LispVal 变量就是返回 LispVal本身。
这里使用 @ 运算符,绑定进行局部匹配的变量:
eval :: LispVal -> LispVal
eval val@(String _) = val
eval val@(Number _) = val
eval val@(Bool _) = val
eval (List [Atom "quote", val]) = val
加入 eval 后的主函数再次体现了 monad 绑定与 . 运算符的组合技巧。
main :: IO ()
main = getArgs >>= putStrLn . show . eval . readExpr . (!! 0)
这个函数中的 ($args) 巧妙使用了 $ 运算符的右结合
apply :: String -> [LispVal] -> LispVal
apply func args = maybe (Bool False) ($ args) $ lookup func primitives
Intermezzo: Error Checking & Exceptions
错误处理一章中展示了 Either 的运用。包括 throwError 和 catchError。
在我们的项目中,用 trapError 方法将 catchError 包装起来。
trapError action = catchError action (return . show)
对于正确的执行结果,我们用 extractValue 析出结果值:
extractValue :: ThrowsError a -> a
extractValue (Right val) = val
eval 中要多处理出错的情况
eval :: LispVal -> ThrowsError LispVal
eval val@(String _) = return val
eval val@(Number _) = return val
eval val@(Bool _) = return val
eval (List [Atom "quote", val]) = return val
eval (List (Atom func : args)) = mapM eval args >>= apply func
eval badForm = throwError $ BadSpecialForm "Unrecognized special form" badForm
函数 primitives 只需要修改返回值声明
primitives :: [(String, [LispVal] -> ThrowsError LispVal)]
…
因为我们不需要修改它的每一种实现,只要修改 unaryOp 和 numericBinop 函数即可:
numericBinop :: (Integer -> Integer -> Integer) -> [LispVal] -> ThrowsError LispVal
numericBinop op singleVal@[_] = throwError $ NumArgs 2 singleVal
numericBinop op params = mapM unpackNum params >>= return . Number . foldl1 op
unaryOp :: (LispVal -> LispVal) -> [LispVal] -> ThrowsError LispVal
unaryOp f [v] = return $ f v
unpackNum 中,我们对无效值作了异常抛出。如果想要严格类型约束,可以将针对 String 和 List 的实现注释掉。
unpackNum :: LispVal -> ThrowsError Integer
unpackNum (Number n) = return n
unpackNum (String n) = let parsed = reads n in
if null parsed
then throwError $ TypeMismatch "number" $ String n
else return $ fst ( parsed !! 0)
unpackNum (List [n]) = unpackNum n
unpackNum notNum = throwError $ TypeMismatch "number" notNum
主函数 main 的实现中,通过 trapError 捕获了可能出现的错误
main :: IO ()
main = do
args <- getArgs
evaled <- return $ liftM show $ readExpr (args !! 0) >>= eval
putStrLn $ extractValue $ trapError evaled
这里对 apply 也做了扩展
apply :: String -> [LispVal] -> ThrowsError LispVal
apply func args = maybe (throwError $ NotFunction "Unrecognized primitives function args" func)
($ args)
(lookup func primitives)
这里利用 Either 进行异常和错误处理的方法值得关注。
Evalution, Part 2
Additional Primitives: Partial Application
类似 unpackNum ,现在我们也有了 unpackStr 和 unpack Num。 notString 和 notBool 的错误值匹配也是一个技巧。
unpackStr :: LispVal -> ThrowsError String
unpackStr (String s) = return s
unpackStr (Number s) = return $ show s
unpackStr (Bool s) = return $ show s
unpackStr notString = throwError $ TypeMismatch "string" notString
unpackBool :: LispVal -> ThrowsError Bool
unpackBool (Bool b) = return b
unpackBool notBool = throwError $ TypeMismatch "boolean" notBool
二元逻辑判断的实现。
boolBinop :: (LispVal -> ThrowsError a) -> (a -> a -> Bool) -> [LispVal] -> ThrowsError LispVal
boolBinop unpacker op args = if length args /= 2
then throwError $ NumArgs 2 args
else do left <- unpacker $args !! 0
right <- unpacker $args !! 1
return $ Bool $ left `op` right
numBoolBinop = boolBinop unpackNum
strBoolBinop = boolBinop unpackStr
boolBoolBinop = boolBinop unpackBool
书中给出的代码有些错误,现在完整的指令表是:
primitives :: [(String, [LispVal] -> ThrowsError LispVal)]
primitives = [("+", numericBinop (+)),
("-", numericBinop (-)),
("*", numericBinop (*)),
("/", numericBinop div),
("=", numBoolBinop (==)),
("<", numBoolBinop (<)),
(">", numBoolBinop (>)),
("/=", numBoolBinop (/=)),
(">=", numBoolBinop (>=)),
("<=", numBoolBinop (<=)),
("&&", boolBoolBinop (&&)),
("||", boolBoolBinop (||)),
("string=?", strBoolBinop (==)),
("string>?", strBoolBinop (>)),
("string<?", strBoolBinop (<)),
("string<=?", strBoolBinop (<=)),
("string>=?", strBoolBinop (>=)),
("mod", numericBinop mod),
("quotient", numericBinop quot),
("remainder", numericBinop rem),
("symbol?", unaryOp symbolp),
("string?", unaryOp stringp),
("number?", unaryOp numberp),
("bool?", unaryOp boolp),
("list?", unaryOp listp)]
Pattern Matching 2
由于 if 语句的实现没有走 primitives ,而是直接取函数名,这里需要将它的 eval 放在前面:
eval :: LispVal -> ThrowsError LispVal
eval val@(String _) = return val
eval val@(Number _) = return val
eval val@(Bool _) = return val
eval (List [Atom "quote", val]) = return val
eval (List [Atom "if", pred, conseq, alt]) =
do result <- eval pred
case result of
Bool False -> eval alt
otherwise -> eval conseq
eval (List (Atom func : args)) = mapM eval args >>= apply func
eval badForm = throwError $ BadSpecialForm "Unrecognized special form" badForm
List Primitives: car, cdr, and cons
对于 car 来说,实现代码与匹配规则一一对应:
(car (a b c)) = a
(car (a)) = a
(car (a b . c)) = a
(car a) = error (not a list)
(car a b) = error (car takes only one argument)
实现为:
car :: [LispVal] -> ThrowsError LispVal
car [List (x : xs)] = return x
car [DottedList (x : xs) _] = return x
car [badArg] = throwError $ TypeMismatch "pair" badArg
car badArgList = throwError $ NumArgs 1 badArgList
同理, cdr 规则
(cdr (a b c)) = (b c)
(cdr (a b)) = (b)
(cdr (a)) = NIL
(cdr (a b . c)) = (b . c)
(cdr (a . b)) = b
(cdr a) = error (not list)
(cdr a b) = error (too many args)
实现为:
cdr :: [LispVal] -> ThrowError LispVal
cdr [List (x:xs)] = return $ List xs
cdr [DottedList (_ :xs) x] = return $ DottedList xs x
cdr [DottedList [xs] x] = return x
cdr [badArg] = throwError $ TypeMismatch "pari" badArg
cdr badArgList = throwError $ NumArgs 1 badArgList
同理添加 cons 和 eqv ,在 primitives 加入对应的 KV 对。
Equal? and Weak Typing: Heterogenous Lists
这里利用了 GHC 的扩展: Existential Types 。使用时需要调用 GHC 的 -fglasgow-exts 选项。
原文这两行似乎写反了。
cdr [DottedList [xs] x] = return x
cdr [DottedList (_ :xs) x] = return $ DottedList xs x
这部分展示了一些使用扩展编译选项的类型推导技术,如 forall。
data Unpacker = forall a . Eq a => AnyUnpacker (LispVal -> ThrowsError a)
forall 的自动类型推导使得弱类型容器成为可能,进一步允许弱类型比较, 最终落实这个运算的是强和弱判等:
eqv :: [LispVal] -> ThrowsError LispVal
eqv [(Bool arg1), (Bool arg2)] = return $ Bool $ arg1 == arg2
eqv [(Number arg1), (Number arg2)] = return $ Bool $ arg1 == arg2
eqv [(String arg1), (String arg2)] = return $ Bool $ arg1 == arg2
eqv [(Atom arg1), (Atom arg2)] = return $ Bool $ arg1 == arg2
eqv [(DottedList xs x), (DottedList ys y)] = eqv [List $ xs ++ [x], List $ ys ++ [y]]
eqv [(List arg1), (List arg2)] = return $ Bool $ (length arg1 == length arg2) &&
(and $ map eqvPair $ zip arg1 arg2)
where eqvPair (x1, x2) = case eqv [x1, x2] of
Left err -> False
Right (Bool val) -> val
eqv [_, _] = return $ Bool False
eqv badArgList = throwError $ NumArgs 2 badArgList
equal :: [LispVal] -> ThrowsError LispVal
equal [arg1, arg2] = do
primitiveEqual <- liftM or $ mapM (unpackEquals arg1 arg2)
[AnyUnpacker unpackNum, AnyUnpacker unpackStr, AnyUnpacker unpackBool]
eqvEquals <- eqv [arg1, arg2]
return $ Bool $ (primitiveEqual || let (Bool x) = eqvEquals in x)
equal badArgList = throwError $ NumArgs 2 badArgList
练习 6.4.1
这个没太大技术含量,我自己就搞定了:
eval (List [Atom "if", pred, conseq, alt]) =
do result <- eval pred
case result of
Bool True -> eval conseq
Bool False -> eval alt
otherwise -> throwError $ TypeMismatch "bool" pred
练习 6.4.2
对 List 进行强和弱类型比较,这里比我想像的复杂,作者先实现了一个 List 比较函数。
eqvList :: ([LispVal] -> ThrowsError LispVal) -> [LispVal] -> ThrowsError LispVal
eqvList eqvFunc [(List arg1), (List arg2)] = return $ Bool $ (length arg1 == length arg2) &&
(all eqvPair $ zip arg1 arg2)
where eqvPair (x1, x2) = case eqvFunc [x1, x2] of
Left err -> False
Right (Bool val) -> val
然后用它重写了 eqv 和 equal 的 List 实现:
eqv [l1@(List arg1), l2@(List arg2)] = eqvList eqv [l1, l2]
eqv [(DottedList xs x), (DottedList ys y)] = eqv [List $ xs ++ [x], List $ ys ++ [y]]
...
equal [l1@(List arg1), l2@(List arg2)] = eqvList equal [l1, l2]
equal [(DottedList xs x), (DottedList ys y)] = equal [List $ xs++[x], List $ ys++[y]]
因为haskell的函数模式匹配是按照书写顺序尝试的,所以要注意 函数的顺序。
练习 6.4.3
作者给出了两种不同的 cond 实现,这里我采用了第一种:
eval (List ((Atom "cond"):cs)) = do
b <- (liftM (take 1 . dropWhile f) $ mapM condClause cs) >>= cdr
car [b] >>= eval
where condClause (List [Atom "else", b]) = return $ List [Bool True, b]
condClause (List [p,b]) = do q <- eval p
case q of
Bool _ -> return $ List [q,b]
_ -> throwError $ TypeMismatch "bool" q
condClause v = throwError $ TypeMismatch "(pred body)" v
f = \(List [p,b]) -> case p of
(Bool False) -> True
_ -> False
根据这个代码,我做出了 case 的实现:
eval (List ((Atom "case"):cs)) = do
b <- (liftM (take 1 . dropWhile f ) $ mapM condClause (tail cs)) >>= cdr
car [b] >>= eval
where cond = cs!!0
condClause (List [Atom "else", b]) = return $ List [Bool True, b]
condClause (List [p,b]) = do x <- eval cond
y <- eval p
q <- eqv [x, y]
case q of
Bool _ -> return $ List [q, b]
_ -> throwError $ TypeMismatch "bool" q
condClause v = throwError $ TypeMismatch "(pred body)" v
f = \(List [p,b]) -> case p of
(Bool False) -> True
_ -> False
至此以后的章节,都没有课后习题了。
Building a REPL: Basic I/O
书中使用了 Parsec 模块的 try ,所以这里不导入 IO 的 try。
import IO hiding (try)
IO Monad 本身已经输出状态了,所以这里用 Monad Then 传递:
flushStr :: String -> IO ()
flushStr str = putStr str >> hFlush stdout
这个 IO Monad then 的运用非常漂亮
readPrompt :: String -> IO String
readPrompt prompt = flushStr prompt >> getLine
摘录: That’s why we write their types using the type variable “m”, and include the type constraint “Monad m =>”
until_ :: Monad m => (a -> Bool) -> m a -> (a -> m ()) -> m ()
until_ pred prompt action = do
result <- prompt
if pred result
then return ()
else action result >> until_ pred prompt action
Adding Variables and Assignment: Mutable State in Haskell
State Monad 对于简单的有状态应用非常好用。
这里使用 state threads,具体而言是 Data.IORef。
import Data.IORef
type Env = IORef [(String, IORef LispVal)]
建立空环境的 nullEnv 函数:
nullEnv :: IO Env
nullEnv = newIORef []
将异常 lift 为自定义的 IOThrowsError
liftThrows :: ThrowsError a -> IOThrowsError a
liftThrows (Left err) = throwError err
liftThrows (Right val) = return val
原文摘录:Methods in typeclasses resolve based on the type of the expression, so throwError and return (members of MonadError and Monad, respectively) take on their IOThrowsError definitions.
函数 runIOThrows 对 trapError 进行了 IO 包装:
runIOThrows :: IOThrowsError String -> IO String
runIOThrows action = runErrorT (trapError action) >>= return . extractValue
函数 lookup 对键值对序列做 key 查询,返回 Maybe value。所以这里利用 const True 做了一个 isBound 查询函数。
isBound :: Env -> String -> IO Bool
isBound envRef var = readIORef envRef >>= return . maybe False (const True) . lookup var
对 IORef 环境的经典读写操作:
getVar :: Env -> String -> IOThrowsError LispVal
getVar envRef var = do env <- liftIO $ readIORef envRef
maybe (throwError $ UnboundVar "Getting an unbound variable" var)
(liftIO . readIORef) (lookup var env)
setVar :: Env -> String -> LispVal -> IOThrowsError LispVal
setVar envRef var value = do env <- liftIO $ readIO envRef
maybe (throwError $ UnboundVar "Setting an unboud variable" var)
(liftIO . (flip writeIORef value))
(lookup var env)
return value
在 bindVars 函数中,利用了 Monadic 管道操作技巧:
bindVars :: Env -> [(String, LispVal)] -> IO Env
bindVars envRef bindings = readIORef envRef >>= extendEnv bindings >>= newIORef
where extendEnv bindings env = liftM (++ env) (mapM addBinding bindings)
addBinding (var, value) = do ref <- newIORef value
return (var, ref)
eval 中传入 env 参数以保持状态,其中 cond 和 case 基本上算重写了一遍……
eval :: Env -> LispVal -> IOThrowsError LispVal
eval env val@(String _) = return val
eval env val@(Number _) = return val
eval env val@(Bool _) = return val
eval env (Atom id) = getVar env id
eval env (List [Atom "quote", val]) = return val
eval env (List [Atom "if", pred, conseq, alt]) =
do result <- eval env pred
case result of
Bool True -> eval env conseq
Bool False -> eval env alt
otherwise -> throwError $ TypeMismatch "bool" pred
eval env (List ((Atom "cond"):cond:cs)) = do
result <- condClause env cond
if (f result)
then eval' env result
else eval env (List ((Atom "cond"):cs))
where condClause env (List [Atom "else", b]) = return $ List [Bool True, b]
condClause env (List [p,b]) = do q <- eval env p
case q of
Bool _ -> return $ List [q,b]
_ -> throwError $ TypeMismatch "bool" q
condClause env v = throwError $ TypeMismatch "(pred body)" v
f = \(List [p,b]) -> case p of
(Bool True) -> True
(Bool False) -> False
eval' = \env (List [p, b]) -> eval env b
eval env (List ((Atom "case"):c:cond:cs)) = do
x <- eval env c
result <- condClause env cond x
if (f result)
then eval' env result
else eval env (List ((Atom "case"):x:cs))
where condClause env (List [Atom "else", b]) x = return $ List [Bool True, b]
condClause env (List [p,b]) x = do y <- eval env p
q <- return $ eqv [x, y]
(\e -> let v = extractValue e
in case v of
Bool _ -> return $List [v, b]
_ -> throwError $ TypeMismatch "bool" v) q
condClause env v x = throwError $ TypeMismatch "(pred body)" v
f = \(List [p,b]) -> case p of
(Bool False) -> False
_ -> True
eval' = \env (List [p, b]) -> eval env b
eval env (List [Atom "set!", Atom var, form]) =
eval env form >>= setVar env var
eval env (List [Atom "define", Atom var, form]) =
eval env form >>= defineVar env var
eval env (List (Atom func : args)) = mapM (eval env) args >>= liftThrows.apply func
eval env badForm = throwError $ BadSpecialForm "Unrecognized special form" badForm
因为要保持环境,单行语句和 REPL 的执行函数分成了两个:
runOne :: String -> IO ()
runOne expr = nullEnv >>= flip evalAndPrint expr
runRepl :: IO ()
runRepl = nullEnv >>= until_ (== "quit") (readPrompt "Lisp>>> ") . evalAndPrint
main :: IO ()
main = do args <- getArgs
case length args of
0 -> runRepl
1 -> runOne $ args !! 0
otherwise -> putStrLn "Program takes only 0 or 1 argument"
Defining Scheme Functions: Closures and Environments
代码示例基本没什么错误 函数的定义和函数执行是两个相关的内容。
扩展后的 LispVal:
data LispVal = Atom String
| List [LispVal]
| DottedList [LispVal] LispVal
| Number Integer
| Float Double
| String String
| Bool Bool
| Character Char
| Ratio Rational
| Complex (Complex Double)
| Vector (Array Int LispVal)
| PrimitiveFunc ([LispVal] -> ThrowsError LispVal)
| Func {params :: [String], vararg :: (Maybe String),
body :: [LispVal], closure :: Env}
| Nil ()
函数定义涉及参数名、参数列、函数体和闭包。新的 apply 实现了函数定义和执行:
函数解析代码中先判定了形参与实参是否一致(如果没有指定动态参数, 给出的实参个数又与形参不同,则报错)。然后利用 curry 方法生成 该函数的函数实现。在各操作(绑定作用域、参数和解析函数体)的过程 中,以 Monad bind 串起各部分。注意这里 bindVars 接受的第一 个函数是 closure ,在 eval 中我们可以看到 closure 是 外部的 env。在传值过程中,env 复制为函数 closure。
apply :: LispVal -> [LispVal] -> IOThrowsError LispVal
apply (PrimitiveFunc func) args = liftThrows $ func args
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
函数相关的 eval 实现。
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
eval env (List (function : args)) = do func <- eval env function
argVals <- mapM (eval env) args
apply func argVals
在 eval 中使用到的函数构造代码:
makeFunc varargs env params body = return $ Func (map showVal params) varargs body env
makeNormalFunc = makeFunc Nothing
makeVarargs = makeFunc . Just . showVal
环境初始化代码进行了进一步的扩展,将所有的 primitive 封装为函数。这部分实现的简洁有力:
primitiveBindings :: IO Env
primitiveBindings = nullEnv >>= (flip bindVars $ map makePrimitiveFunc primitives)
where makePrimitiveFunc (var, func) = (var, PrimitiveFunc func)
runOne :: String -> IO ()
runOne expr = primitiveBindings >>= flip evalAndPrint expr
runRepl :: IO ()
runRepl = primitiveBindings >>= until_ (== "quit") (readPrompt "Lisp>>> ") . evalAndPrint
最后这几章的内容中,数次出现了 flip 操作。
Creating IO Primitives: File I/O
这一章代码没什么争议 因为输入的参数不同(数组和数值的区别),io 的指令集是独立实现的:
ioPrimitives :: [(String, [LispVal] -> IOThrowsError LispVal)]
ioPrimitives = [("apply", applyProc),
("open-input-file", makePort ReadMode),
("open-output-file", makePort WriteMode),
("close-input-port", closePort),
("close-output-port", closePort),
("read", readProc),
("write", writeProc),
("read-contents", readContents),
("read-all", readAll)]
其底层的文件操作都是对 haskell 库的浅封装:
applyProc :: [LispVal] -> IOThrowsError LispVal
applyProc [func, List args] = apply func args
applyProc (func : args) = apply func args
readProc :: [LispVal] -> IOThrowsError LispVal
readProc [] = readProc [Port stdin]
readProc [Port port] = (liftIO $ hGetLine port) >>= liftThrows . readExpr
writeProc :: [LispVal] -> IOThrowsError LispVal
writeProc [obj] = writeProc [obj, Port stdout]
writeProc [obj, Port port] = liftIO $ hPrint port obj >> (return $ Bool True)
readContents :: [LispVal] -> IOThrowsError LispVal
readContents [String filename] = liftM String $ liftIO $ readFile filename
makePort :: IOMode -> [LispVal] -> IOThrowsError LispVal
makePort mode [String filename] = liftM Port $ liftIO $ openFile filename mode
closePort :: [LispVal] -> IOThrowsError LispVal
closePort [Port port] = liftIO $ hClose port >> (return $ Bool True)
closePort _ = return $ Bool False
load :: String -> IOThrowsError [LispVal]
load filename = (liftIO $ readFile filename) >>= liftThrows . readExprList
readAll :: [LispVal] -> IOThrowsError LispVal
readAll [String filename] = liftM List $ load filename
为了处理单语句和多语句,代码解析也进行了抽象,分别实现具体的操作
readOrThrow :: Parser a -> String -> ThrowsError a
readOrThrow parser input = case parse parser "lisp" input of
Left err -> throwError $ Parser err
Right val -> return val
readExpr = readOrThrow parseExpr
readExprList = readOrThrow (endBy parseExpr spaces)
环境初始化,引入指令集的实现,针对 IO 作了扩展:
primitiveBindings :: IO Env
primitiveBindings = nullEnv >>= (flip bindVars $ map (makeFunc IOFunc) ioPrimitives
++ map (makeFunc PrimitiveFunc) primitives)
where makeFunc constructor (var, func) = (var, constructor func)
交互过程做了相应的调整
runOne :: [String] -> IO ()
runOne args = do
env <- primitiveBindings >>= flip bindVars [("args", List $ map String $ drop 1 args)]
(runIOThrows $ liftM show $ eval env (List [Atom "load", String (args !! 0)]))
>>= hPutStrLn stderr
runRepl :: IO ()
runRepl = primitiveBindings >>= until_ (== "quit") (readPrompt "Lisp>>> ") . evalAndPrint
main :: IO ()
main = do args <- getArgs
if null args then runRepl else runOne $ args
Towards a Standard Library: Fold and Unfold
这一章只有 scheme 的代码和内置库实现,留待想要学习 sheme 语言的时候深入。
Conclusion & Further Resources
附录跳过
总结
时间和精力有限,我没有认真的去做每一道题,而仅仅是把在线答案中的每一个 调试验证了一遍。示例中还是有一些小错误的,如果只是复制,无法顺利编译运 行,有一些我在笔记中有提到,附件中是我自己的练习代码。
从我个人体验来讲,用 ghci 来练习,要比编译后调试要快捷得多。不过从第 六章 Evalution 2 开始,需要 -XExistentialQuantification 选项。 如果你和我一样使用 emacs 的 ghci 集成 shell ,没办法用 ghci -XExistentialQuantification 形式启动交互环境,可以在 ghci 中执行 :set -XExistentialQuantification 加载这一选项。
这本书不应该作为 Haskell 学习的第一本书,甚至也不一定应该是第二本书。 学习此书最好有初步的 lisp/scheme 知识。
我的阅读过程还是比较粗糙的,建议在遇到不能完全理解的地方,用 ghci 调试 一下。
作为第一本书,Yet Another Haskell Tutorial 更系统,内容循序渐进。
作为第二本书,Real World Haskell 更有实践性,曲线比较平滑。
本书应该作为从初阶到中阶的一本阶段总结辅导。或者,除非你有 scheme/lisp 基础,对第一本书也学的比较顺利……