More Scheming with Haskell
∞It's been a little while since I wrote about Haskell and the Scheme interpreter I've been using to learn and play with both Haskell and Scheme. I finished the tutorial and got myself a working Scheme interpreter and indeed it has been fun to use it for trying out little things now and then. (Normally I would use Emacs or Dr. Scheme for that sort of thing.) There certainly are interesting things to try floating around da intranet. And also things to read and learn from, such as misp (via Moonbase).
I'm going to describe two new features of my Scheme in this post. The second one is more interesting and was more fun to implement (cond).
Pasing Scheme integers
Last time I left off at parsing R5RS compliant numbers, which is exercise 3.3.4 if you're following along the tutorial. Only integers in binary, octal, decimal, and hexadecimal are parsed right now. The syntaxes for those are #b101010
, #o52
, 42
(or #d42
), and #x2a
, respectively. To parse these we use the readOct
, readDec
, readHex
, and readInt
functions provided by the Numeric module, and import them thusly:
import Numeric (readOct, readDec, readHex, readInt)
In order to parse binary digits we need to write a few short functions to help us out. For some reason I couldn't find binDigit
, isBinDigit
and readBin
in their respective modules but luckily they're trivial to implement. The first two are self-explanatory, as is the third if you look at the implementation of its relatives for larger bases. In a nutshell readBin
says to: "read an integer in base 2, validating digits with isBinDigit
."
-- parse a binary digit, analagous to decDigit, octDigit, hexDigit
binDigit :: Parser Char
binDigit = oneOf "01"
-- analogous to isDigit, isOctdigit, isHexDigit
isBinDigit :: Char - Bool
isBinDigit c = (c == '0' || c == '1')
-- analogous to readDec, readOct, readHex
readBin :: (Integral a) = ReadS a
readBin = readInt 2 isBinDigit digitToInt
The next step is to augment parseNumber
so that it can handle R5RS numbers in addition to regular decimal numbers. To refresh, the tutorial's parseNumber
function looks like this:
parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit
Three more lines in this function will give us a decent starting point:
parseNumber = do char '#' base <- oneOf "bdox"
parseDigits base
Translation: First look for an R5RS style base, and if found call parseDigits
with the given base to do the dirty work. If that fails then fall back to parsing a boring old string of decimal digits.
That brings us to actually parsing the numbers. parseDigits
is simple, but there might be a more Haskell-y way of doing this.
-- Parse a string of digits in the given base.
parseDigits :: Char - Parser LispVal
parseDigits base = many1 d >>= return
where d = case base of
'b' -> binDigit
'd' -> digit
'o' -> octDigit
'x' -> hexDigit
The trickiest part of all this was figuring out how to use the various readFoo
functions properly. They return a list of pairs so head
grabs the first pair and fst
grabs the first element of the pair. Once I had that straight it was smooth sailing. Having done this, parsing R5RS characters (#\a, #\Z, #\?, ...) is a breeze so I won't bore you with that.
### The cond function ###
It still takes me some time to knit together meaningful Haskell statements. Tonight I spent said time cobbling together an implementation of cond as a new special form. Have a look at the code. The explanation follows.
1 2 3 4 5 6 7 8 9
eval env (List (Atom "cond" : List (Atom "else" : exprs) : [])) =
liftM last $ mapM (eval env) exprs
eval env (List (Atom "cond" : List (pred : conseq) : rest)) =
do result <- eval env $ pred
case result of
Bool False -> case rest of
[] -> return $ List []
_ -> eval env $ List (Atom "cond" : rest)
_ -> liftM last $ mapM (eval env) conseq
* __Lines 1-2:__ Handle else
clauses by evaluating the given expression(s), returning the last result. It must come first or it's overlapped by the next pattern.
* __Line 3:__ Evaluate a cond
by splitting the first condition into predicate and consequence, tuck the remaining conditions into rest
for later.
* __Line 4:__ Evaluate pred
* __Line 5:__ and if the result is:
* __Line 6:__ #f
then look at the rest of the conditions.
* __Line 7:__ If there are no more conditions return the empty list.
* __Line 8:__ Otherwise call ourselves recursively with the remaining conditions.
* __Line 9:__ Anything other than #f
is considered true and causes conseq
to be evaluated and returned. Like else
, conseq
can be a sequence of expressions.
So far my Scheme weighs in at 621 lines, 200 more than the tutorial's final code listing. Hopefully I'll keep adding things on my TODO list and it will grow a little bit more. Now that I have cond
it will be more fun to expand my stdlib.scm as well.