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
#x2a, respectively. To parse these we use the
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
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
-- 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
elseclauses 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
condby splitting the first condition into predicate and consequence, tuck the remaining conditions into
Line 4: Evaluate
- Line 5: and if the result is:
#fthen 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
#fis considered true and causes
conseqto be evaluated and returned. Like
conseqcan 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.