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