Write Yourself a Scheme in 48 Hours in F# – Part IV
Luca -
☕ 3 min. read
It is the evaluator turn. It is a big file, let’s see if I can fit it in a single post.
Aptly enough, the most important function is called eval.
eval env = function | String _ as v -> v | Number _ as v -> v | Bool _ as v -> v | Atom var -> getVar var env | List [Atom "quote"; v] -> v | List [Atom "if"; pred; conseq; alt] -> evalIf env pred conseq alt | List [Atom "load"; fileName] -> load [fileName] |> List.map (eval env) |> last | List [Atom "set!" ; Atom var ; form] -> env |> setVar var (eval env form) | List [Atom "define"; Atom var; form] -> define env var (eval env form) | List (Atom "define" :: (List (Atom var :: parms) :: body)) -> makeNormalFunc env parms body |> define env var | List (Atom "define" :: (DottedList ((Atom var :: parms), varargs) :: body)) -> makeVarargs varargs env parms body |> define env var | List (Atom "lambda" :: (List parms :: body)) -> makeNormalFunc env parms body | List (Atom "lambda" :: (DottedList(parms, varargs) :: body)) -> makeVarargs varargs env parms body | List (Atom "lambda" :: ((Atom _) as varargs :: body)) -> makeVarargs varargs env [] body | List (func :: args) -> let f = eval env func let argVals = List.map (eval env) args apply f argVals | badForm -> throw (BadSpecialForm("Unrecognized special form", badForm))
This is the core of the evaluator. It takes as an input the LispVal generated by the parser and an environment and returns a LispVal that is the result of the reduction. As a side effect, it occasionally modify the environment. I carefully crafted the previous phrase to maximize the discomfort of the functional programmers tuned in. Such fun 🙂
More seriously (?), here is what it does:
- If it is a String, Number of Bool, just return it
- If it is an Atom, return its value
- If it is a quote statement, return what is quoted (read your little schemer manual)
- If it is an if statement, evaluate it (see below)
- If it is a load statement, load the file (see below) and evaluate each expression using the current environment. Return the last expression in the file
- If it is a set!, set the value of the passed variable to the evaluated form
- If it is a define, do almost the same as above (except that you don’t throw if the variable doesn’t exist, but you create it)
- If it is a define that defines a function (it has that shape), create a ‘function slot’ in the environment. That is a (functionName, FuncRecord) pair (see below)
- If it is a lambda, return the FuncRecord that describe the inline function
- If it is a function call, evaluate the expression that describe the function (yes, you can do that in Lisp!!), evaluate the arguments, and apply the function to the arguments
- Otherwise, it must be a bad form, throw it back to the calling function to do something meaningful with it
We have a bunch of ‘see below’ to take care of. We’ll look at them in order.
First the ‘if’ statement. If the evaluated predicate is Bool(True) evaluate the consequent, otherwise evaluate the alternative. For some reason, I wrote it the other way around.
and // 1a. If the evaluation of the pred is false evaluate alt, else evaluate cons evalIf env pred conseq alt = match eval env pred with | Bool(false) -> eval env alt | _ -> eval env conseq
Then there is the load function. It reads all the test and gets out the list of LispVal contained in it.
let load = fileIOFunction (fun fileName -> File.ReadAllText(fileName) |> readExprList)
ReadExprList is part of the parser. We’ll look at it then. Sufficient here to say that it takes a string and returns a list of LispVal.
FileIOFunction just encapsulates a common pattern in all the file access functions. I don’t like such mechanical factorization of methods, without any real reusability outside the immediate surroundings of the code. But I like repetition even less.
let fileIOFunction func = function | [String fileName] -> func (fileName) | [] -> throw (IOError("No file name")) | args -> throw (NumArgs(1, args))
A family of functions create FuncRecord given appropriate parameters. I seem to have lost memory of the contortions related to the last one. If I end up having to work again on this code, I’ll need to figure it out again. Note to myself, please comment this kind of code next time.
let makeFunc varargs env parms body = Func ({parms = (List.map showVal parms); varargs = varargs; body = body; closure = env}) let makeNormalFunc = makeFunc None let makeVarargs = showVal >> Some >> makeFunc
apply is the other workhorse function in the evaluator. The best way to understand it is to start from the bottom (where bindVars starts the line). We are binding the arguments and the variable arguments in the closure that has been passed in. We then evaluate the body. But the body is just a list of LispVal, so we just need to evaluate them in sequence and return the result of the last one.
and apply func args = match func with | PrimitiveFunc(f) -> f args | Func ({parms = parms; varargs = varargs; body = body; closure = closure}) -> let invalidNonVarargs = args.Length <> parms.Length && varargs.IsNone let invalidVarargs = args.Length < parms.Length && varargs.IsSome if invalidVarargs || invalidNonVarargs then throw (NumArgs(parms.Length, args)) else let remainingArgs = args |> Seq.skip parms.Length |> Seq.toList let evalBody env = body |> List.map (eval env) |> last let rec zip xs1 xs2 acc = match xs1, xs2 with | x1::xs1, x2::xs2 -> zip xs1 xs2 ((x1, x2)::acc) | _ -> acc let bindVarArgs arg env = match arg with | Some(argName) -> bindVars [argName, (List remainingArgs)] env | None -> env bindVars (zip parms args []) closure |> bindVarArgs varargs |> evalBody | funcName -> throw (NotFunction("Expecting a function, getting ", showVal funcName))
This is enough for one post. Next time we’ll finish the evaluator.
0 Webmentions
These are webmentions via the IndieWeb and webmention.io.