samhuri.net


By Sami Samhuri

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.