(Writing Lispelm in Elm ;)
Background
So a coworker the other day in the slack channels had shared some links to SICP and the associated lecture on the metacircular evaluator.
I thought that another good addition to the resources provided would be Peter Norvig's small Lisp implementation in Python. And I got the thinking I haven't coded elm in a while let me see if I can write and get going.
I was also thinking that I could add to the introductory material on working in elm.
Installing Elm
Getting a copy of elm is easy!
You can either
Download your installer from install page
If you have node installed you can
npm install elm
Getting Started
Once you have a copy of elm you want to create a new directory for your project.
mkdir lispelm
Init elm
Using elm init which comes installed with elm we can initialize our elm project.
cd lispelm && elm init
It will ask you if you are ok with some stuff and then point you to read the documentation. I also recommend you giving the official documentation a look over.
Say yes and let it do its thing.
If we see whats our director now
ls
elm.json src
We have an elm.json
file which stores information about our dependencies, project metadata and other things.
So next we want to create a file in the src
directory to work out of
touch src/Main.elm
Peter Norvig Lis.py
Peter Norvig has done implementations of programming languages in Python, Common Lisp and even Java. You should totally go read some of his stuff over at his blog. I will be loosely basing my approach and style off his work.
Given the nature of Elm being statically typed we may have a little easier time but this is a guess.
What do you mean by that?
Essentially we will be writing a function in elm that takes as input expression
and returns or evaluates to a value. This function has a fitting name an evaluate
`.
So we have identified our input and output and a function name. We have enough to write a type spec:
eval : Expr -> Val
I changed evaluate
to eval
and Value
to Val
.
Side Quest: Building a Calculator
Super easy what does a calculator have? Numbers yeah and operations on those numbers
Can we build a language that describes the calculator?
Well Sure lets go back to the really basic calculators first though, Lets only consider Add because why not.
So what meaning do we usually get from a calculator?
Numbers !
So our calculator(evaluator) will return numbers.
Our calculator is simple. It has numbers and 2 operations (+) and (=). We can think of the calculator as having its own language comprising just of Add and the different Numbers.
Well what is equal there for?
Well that's how we translate from CalcLang to the calculator screen usually?
Its Essentially
"What do You mean by that"
Alright enough talk lets write some code, having some foresight into me knowing I'm going to mess up ill call this type my draft One. Our first attempt at modeling our calculator may look like this
type alias Digit = Int
type CalcLanDraftOne = Add Digit Digit |Val Digit
Using our imaginary calculator with only numbers and the "+" button does our type allow us to express everything we can press?
Well lets press "1" then press "+" then press "3" well how might we express** (cough cough foreshadow)
these action in our CalcLang draft.
Add (Val 1) (Val 3)
Add (Val 1) (Val 3) : CalcLanDraftOne So far so good now what about this
Lets press "1" then press "+" then press "1" then press "+" then press "1"
Can you express this in CalcLanDraftOne ?
Nope ?
To keep a history just comment the above type out
Lets try again,
type CalcLanDrafTwo = Add CalcLanDrafTwo CalcLanDraftTwo | Val Digit
Can we express that statement now ? I think so
Add 1 (Add 1 1)
Add (Val 1) (Add (Val 1) (Val 1)) Add (Val 1) (Add (Val 1) (Val 1))
: CalcLanDraftTwo
So in CalcLangDraftTwo we seem to be ok for now. Since we have the language kind of understood with our model lets see
What do you mean by that?
Lets write calc or eval
eval : CalcLanDrafTwo -> Number
eval calcLangExpression =
case calcLangExpression of
Add expr1 expr2 ->
eval expr1 + eval expr2
s -> s
Note how the meaning of Adding two expressions only relies on the meaning of its arguments, this is a property we will want to note for later.
I just want to take a brief note here because this is only one of many answers to the question
What do you mean by that?
Can you think of another meaning to derive from the CalcLanDrafTwo?
print: CalcLanDrafTwo -> String
print calcLangExpression =
case calcLangExpression of
Add expr1 expr2 ->
String.concat [print expr1,"+",print expr2]
s -> String.fromInt s
We have a couple of semantics defined over our CalcLang or two answers to
What does that even mean ?
I'm remembering from high school my TI-84, I could go up and down and look at my old results?
Some could say we had History
Does this modify our language?
I don't think so its more of the meaning changes, if you bring history and some language can you describe history? I think you can, people always bias their meaning with history and it adds to their story....
evalWithHistory : CalcLanDrafTwo -> History -> History
Can we make it so that our evalWithHistory
function if we looked at the History returned and get the first story that is equal to the value of our expression? In code :
evalWithHistory expr someHistory = placeIntoHistory (eval expr) someHistory
Why yes lets just go from the definition
We have two cases in our CalcLanDrafTwo:
a Number
the addition of two calcLangExpression
Lets tackle case 1 first
Well if we our trying to evaluate a number with some history ,isn't that just adding the number to our history
evalWithHistory 3 someHistory = placeIntoHistory (eval 3) someHistory
and then because eval 3 = 3
evalWithHistory 3 someHistory = placeIntoHistory 3 someHistory
Lets tackle case 2
We try to evaluate an Add expression that adds two expressions
evalWithHistory (Add x y) someHistory = placeIntoHistory (eval (Add x y)) someHistory
and then because eval (Add x y) = eval x + eval y
evalWithHistory (Add x y) someHistory = placeIntoHistory (eval x + eval y)) someHistory
Well we could say stop here but remembering that property I noted earlier how eval evaluated the Add case in terms only of its meaning. Can we do that here?
Well keeping that in mind what do we know so far well we have a relationship between eval
and evalWithHistory
evalWithHistory exp history = placeIntoHistory (eval exp) history
we can also go the other way eval our exp is the same as getting the most recent element from our history list that we got from evalWithHistory exp of noHistory.
eval exp = getFirstFromHistory (evalWithHistory exp noHistory)
Can we use this information to help us?
evalWithHistory (Add x y) someHistory = placeIntoHistory (eval x + eval y)) someHistory
= placeIntoHistory (getFirstFromHistory(evalWithHistory x someHistory) + eval y)) someHistory
we can replace eval y
similarly
= placeIntoHistory
(getFirstFromHistory(evalWithHistory x someHistory) +
(getFirstFromHistory(evalWithHistory y someHistory)) someHistory
But wait instead of passing someHistory twice we can put them both on the stack and then add the first two numbers of the stack putting that back on the stack.
= addFirstTwoHistory
(evalWithHistory x (evalWithHistory y someHistory))
So that works pretty well we can repeatedly call evalWithHistory and keep a record of what happened.
Ok so like this wasnt a thing in my TI-84 but imagine we want to just be able to delay our calculations you know if we had like a really lon calculation. unlikely but work with mem here.
We can use Continuations, a continuation is sort of like a promise but it a function that takes a function as an argument to essentially tell it what to do next.
Essentially we want a function that meets this requirement
evalWithDelay expr cont history =
cont (evalWithHistory expr history)
Lets break this down again we have our two cases a value and an addition
. = cont ( evalWithHistory (Val 3) history ) = cont ( placeIntoHistory (Val 3) history )
That was the easy case now letts do the addition
cont (evalWithHistory (Add x y) history)
= cont (addFirstTwoHistory (evalWithHistory x (evalWithHistory y history)))
= (cont << addFirstTwoHistory) (evalWithHistory x (evalWithHistory y history))
Now notice that we are doing one thing after evaluating the other so in other words
=
evalWithDelay x (cont << addFirstTwoHistory) (evalWithHistory y history)
And notice again
=
evalWithDelay x (evalWithDelay y (cont << addFirstTwoHistory) history)
So this works we get delayed calculation
evalWithDelay (Val 3) id
: History -> History
Playing around with what we have so far yields interesting things
evalWithDelay (Val 3) id
evalWithDelay (Val 3) addFirstTwoHistory
evalWithDelay (Val 3) ((::) 3)
You should put these in the repl and see what happens when you pass these a [2]
Now I like functions and continuations as much as everyone else but I would rather be able to deal with something else to pass as args.
Code Baby
type Code = Halt | AddS Code | Push Code
Here Code represents our delayed computation (continuation) so instead of gettin our answers immediately like we said earllier we can delay it and get stuff later. To help us to do this we give thes delays a represeentation (code).
Now to execute this code would be to actually get answers weve made it a long way from out interpreter.
Lets write an execute function
execute : Code -> History -> History
execute code =
case code of
Halt -> halt
Push n c -> pushC n (execute c)
AddS c -> addC (execute c)
pushC : Digit -> (History -> History) -> History -> History
pushC n c = c << (::) n
addC : (History -> History) -> History -> History
addC c = c << addHistory
compC : Expr -> Code -> Code
compC expr cont =
case expr of
Val n -> Push n cont
Add e1 e2 -> compC e1 (compC e2 (AddS cont ))
As you can see our execute code just returns continuations and for us to actually ggget an answer we need to stop our machine.