Write IN THE SCHEME
Trials and tribulations of building a Scheme in Haskell
( )
Start with a proof ;)
Yes it does actually work !
nawwww you can curry functions! *pat pat*
Things to remember
- There is a LOT of hand-waving that goes on.
- IORef / Lift
- Surprise! New operators!
- This happens A LOT.. (>>) , (>>=) , (<|>)
Just keep moving !
tips for starting
Cabal Sandboxes are your best friend... EVER.
# Create your working folder
$ mkdir mylisp
$ cd mylisp
# Init a sandbox using `cabal`
$ cabal sandbox init
# follow the prompts!
Not clumsy or inaccurate like a global install...
A more elegant workflow, for a more civilised project.
Sanity maintained !
You can then take advantage of 'sandbox' to maintain your sanity!
// scan and check your 'mylisp.cabal' $ cabal configure
//Keep your libs contained/updated $ cabal install --only-dependencies // build your project to a subfolder $ cabal install // run a project specific REPL! $ cabal repl
haskell project with a sandbox
Haskell project without a sandbox
the CODE in the book is correct
It's highly unlikely you will find bugs in the book.
If you're getting errors it's because you've typed it in incorrectly or you have done something else wrong.
Take a deep breath.
Follow the error message(s) and compare to the book.
Except for "System.Error", that doesn't exist anymore... Use "Control.Monad.Error"
Parsing is fun
lift juggling in the IO Monad is not
The book contains examples of code you should probably aspire to write, as well as plenty of examples of code you probably shouldn't.
writing the parser is fun
The ParsecCombinators parser is a lot of fun to use.
The parser can be broken up into sensible sections.
It's subjective, but I consider this to be very nice Haskell code.
-- Read an inline comment inlineComment :: Parser String inlineComment = try $ string ";;" >> manyTill anyChar (try newline)
-- Ignoring comments comments :: Parser () comments =
skipMany1 (blockComment <|> inlineComment)
The hand-waving
Due to the use of IORef for storing evaluation state, there is a need for almost the entire evaluator to operate inside the IO Monad.
defineVar :: Env -> String -> LispVal -> IOThrowsError LispVal
defineVar envRef var value = do
alreadyDefined <- liftIO $ isBound envRef var
if alreadyDefined
then setVar envRef var value >> return value
else liftIO $ do
valueRef <- newIORef value
env <- readIORef envRef
writeIORef envRef ((var, valueRef) : env)
return value
But keep moving !
Don't sweat it for now, just get your language up and running!
building a repl
This part was very exciting !
You really start to wire everything up now.
weeeee !!1111(eleventy)
$ cabal install $ ./dist/build/lisp/lisp Lisp>>> (+ 2 3) 5
Lisp>>> (cons this '()) Unrecognized special form: this
Lisp>>> (cons 2 3) (2 . 3)
Lisp>>> (cons 'this '()) (this)
Lisp>>> quit
implementing functions !
This section has you defining a type and associated functions for allowing you to define your own functions.
Starting with ensuing Primitive Functions are available.
Then you implement a technique for defining and storing your own.
Again with the scary code
apply :: LispVal -> [LispVal] -> IOThrowsError LispVal
apply (PrimitiveFunc func) args = liftThrows $ func args
apply (Func parms varargs body closure) args =
if num parms /= num args && isNothing varargs
then throwError $ NumArgs (num parms) args
else liftIO (bindVars closure $ zip parms args) >>= bindVarArgs varargs >>= evalBody
where
remainingArgs = drop (length parms) args
num = toInteger . length
evalBody env = liftM last $ mapM (eval env) body
bindVarArgs arg env = case arg of
Just argName -> liftIO $ bindVars env [(argName, List remainingArgs)] Nothing -> return env
writing your own std lib !!
my std leeb for my leesp
(define (not x)
;; Flip a boolean value or result.
(if x #f #t))
(define (null? obj)
;; Check if the given arg is null
(if (eqv? obj '()) #t #f))
(define (curry func arg1)
;; Create a partially applied function
;; and its first argument.
(lambda (arg) (func arg1 arg)))
Writing your own stdlib is quite satisfying !
All those functions you take for granted, you now have to write!
now what ?
Celebrate!
Bask in your amazing abilities as you realise you have implemented a programming language !
enough basking
geddit?
>.>
When everything is finished,
start working through the exercises!
How to improve?!
You now have a wonderful test bed to apply new things that you learn !!
For example: I read the Monad Transformers paper and I was able to apply this to my Leesp !
To demonstrate... Because of this:
type Env = Map.Map String LispVal
-- Spooky monad transformer of awesome:
type Eval a = ReaderT Env (ErrorT LispError (StateT Env Identity)) a
This :
getVar :: Env -> String -> IOThrowsError LispVal
getVar envRef var = do
env <- liftIO $ readIORef envRef
maybe (throwError $ UnboundVar "Getting an unbound variable" var)
(liftIO . readIORef)
(lookup var env)
setVar :: Env -> String -> LispVal -> IOThrowsError LispVal
setVar envRef var value = do
env <- liftIO $ readIORef envRef
maybe (throwError $ UnboundVar "Setting an unbound variable" var)
(liftIO . (`writeIORef` value))
(lookup var env)
return value
Became this !
setVar ∷ String → LispVal → Eval LispVal
setVar var value =
do env ← ask
case Map.lookup var env of
Nothing → defineVar var value
Just _ → throwError $ BoundVar "Variable already bound" value
defineVar ∷ String → LispVal → Eval LispVal
defineVar var value = modify (Map.insert var value) >> return value
And this:
myCaseFun :: Env -> [LispVal] -> LispVal -> IOThrowsError LispVal
myCaseFun _ [] selector = liftThrows . throwError $ BadSpecialForm "Non-exhaustive patterns in " selector
myCaseFun env (comparator:conseq:rest) selector = do
choice <- if caseHasAtom comparator
then return comparator
else eval env comparator
comparison <- liftThrows $ eqv [selector,choice]
case comparison of
Bool True -> eval env conseq
Bool False -> myCaseFun env rest selector
where
caseHasAtom (Atom _) = True
caseHasAtom _ = False
became this:
evCaseFun ∷ [LispVal] → LispVal → Eval LispVal
evCaseFun [] selector = throwError $ BadSpecialForm "Non-exhaustive patterns in " selector
evCaseFun (comparator:conseq:rest) selector = do
choice ← if caseHasAtom comparator then return comparator else eval comparator
equiv ← eqv [selector,choice]
case equiv of
Bool True → eval conseq
Bool False → evCaseFun rest selector
where
caseHasAtom (Atom _) = True
caseHasAtom _ = False
Why haven't you started yet?
Write Yourself a Scheme is a great tutorial for burgeoning Haskelliers that are moving beyond the normal material.
There is plenty of scope to really make the project your own.
Who says the syntax even has to be a lisp?
Can you implement macros?
What about concurrency?
I'll shut up and give you some links
The wikibook:
The Github Community:
My Leesp:
Monad Transformers:
Thanks !
To the creators and subsequent editors of the WikiBook for such a fun tutorial!
&&
(all the pony gifs are the fault of the person that inspired me to try this in the first place..... somehow...)
Write in the Scheme
By mankykitty
Write in the Scheme
Trials and tribulations of building a Scheme in Haskell
- 1,384