Achievement Unlocked

A Better Path to Language Learning

Midwest.io 2015

Joel Martin

Press 's' to show speaker notes

About Me

Joel Martin (kanaka)

github.com/kanaka

@bus_kanaka

Principal Software Engineer at ViaSat, Inc

Clojure

Satellites!

Welcome to Polyglossia

  • Groovy (2003)
  • Scala (2004)
  • Go (2009)
  • Swift (2014)
  • Clojure (2007)
  • Rust (2010)
  • Julia (2012)
  • Dart (2013)
  • Factor (2003)
  • Nim (2008)
  • Kotlin (2011)
  • Crystal (2012)

The Best Languages for the   Organization Task/Project System

It's a Polyglot World

Language learning is part of the job.

Learning is work!

Work is inherently difficult

The typical process:

  • Hello world
  • Function definition and calling
  • Basic data types
  • Basic flow control constructs
  • Etc, etc, ad naseum

... or is it?

“Why will people pay for the privilege of working harder than they will work when they are paid?
-- The Game of Work: Charles Coonradt”

"Work" is not a thing, it's a state of mind.

The Inspiration

  • Gherkin
  • lispy

Mal Beginnings

JS


...
function read_form(reader) {
    var token = reader.peek();
    switch (token) {
    // reader macros/transforms
    case ';': return null; // Ignore comments
    case '\'': reader.next();
               return [types._symbol('quote'), read_form(reader)];
    case '`': reader.next();
              return [types._symbol('quasiquote'), read_form(reader)];
    case '~': reader.next();
              return [types._symbol('unquote'), read_form(reader)];
...
            

GNU Make


...
define READ_FORM
$(and $(READER_DEBUG),$(info READ_FORM: $($(1))))
$(call READ_SPACES,$(1))
$(foreach ch,$(word 1,$($(1))),\
  $(if $(filter $(SEMI),$(ch)),\
    $(call DROP_UNTIL,$(1),$(_NL)),\
  $(if $(filter $(SQUOTE),$(ch)),\
    $(eval $(1) := $(wordlist 2,$(words $($(1))),$($(1))))\
    $(call _list,$(call _symbol,quote) $(strip $(call READ_FORM,$(1)))),\
  $(if $(filter $(QQUOTE),$(ch)),\
    $(eval $(1) := $(wordlist 2,$(words $($(1))),$($(1))))\
    $(call _list,$(call _symbol,quasiquote) $(strip $(call READ_FORM,$(1)))),\
  $(if $(filter $(UNQUOTE),$(ch)),\
...
            

"Make Lisp"

Bash


...
READ_FORM () {
  local token=${__reader_tokens[${__reader_idx}]}
  case "${token}" in
    \')   __reader_idx=$(( __reader_idx + 1 ))
          _symbol quote; local q="${r}"
          READ_FORM; local f="${r}"
          _list "${q}" "${f}" ;;
    \`)   __reader_idx=$(( __reader_idx + 1 ))
          _symbol quasiquote; local q="${r}"
          READ_FORM; local f="${r}"
          _list "${q}" "${f}" ;;
...
            

C


...
MalVal *read_form(Reader *reader) {
    char *token;
    MalVal *form = NULL, *tmp;
    token = reader_peek(reader);
    if (!token) { return NULL; }
    switch (token[0]) {
    case ';':
        abort("comments not yet implemented");
        break;
    case '\'':
        reader_next(reader);
        form = _listX(2, malval_new_symbol("quote"),
                         read_form(reader));
        break;
...
            

Python


...
def read_form(reader):
    token = reader.peek()
    # reader macros/transforms
    if token[0] == ';':
        reader.next()
        return None
    elif token == '\'':
        reader.next()
        return _list(_symbol('quote'), read_form(reader))
    elif token == '`':
        reader.next()
        return _list(_symbol('quasiquote'), read_form(reader))
...
            

Clojure


...
;; Override some tools.reader reader macros so that we can do our own
;; metadata and quasiquote handling
(alter-var-root #'r/macros
  (fn [f]
    (fn [ch]
      (case ch
        \` (wrap 'quasiquote)
        \~ (fn [rdr comma]
             (if-let [ch (rt/peek-char rdr)]
               (if (identical? \@ ch)
                 ((wrap 'splice-unquote) (doto rdr rt/read-char) \@)
                 ((wrap 'unquote) rdr \~))))
...
            

PHP


...
function read_form($reader) {
    $token = $reader->peek();
    switch ($token) {
    case '\'': $reader->next();
               return _list(_symbol('quote'),
                               read_form($reader));
    case '`':  $reader->next();
               return _list(_symbol('quasiquote'),
                               read_form($reader));
    case '~':  $reader->next();
               return _list(_symbol('unquote'),
                               read_form($reader));
...
            

Java


...
    public static MalVal read_form(Reader rdr)
            throws MalContinue, ParseError {
        String token = rdr.peek();
        if (token == null) { throw new MalContinue(); }
        MalVal form;

        switch (token.charAt(0)) {
            case '\'': rdr.next();
                       return new MalList(new MalSymbol("quote"),
                                          read_form(rdr));
            case '`': rdr.next();
                      return new MalList(new MalSymbol("quasiquote"),
                                         read_form(rdr));
...
            

An Accidental Discovery

"Make Lisp" became "Make-A-Lisp"

Mal Itself

Demo Time

Mal and More Mal

PostScript


...
% read_form: read the next form from string start at idx
/read_form { 3 dict begin
    read_spaces
    /idx exch def
    /str exch def

    idx str length ge { null str idx }{ %if EOF

    /ch str idx get def  % current character
    ch 39 eq { %if '\''
        /idx idx 1 add def
        str idx read_form
        3 -1 roll   /quote exch 2 _list   3 1 roll
...
            

C#


...
        public static MalVal read_form(Reader rdr) {
            string token = rdr.peek();
            if (token == null) { throw new MalContinue(); }
            MalVal form = null;

            switch (token) {
                case "'": rdr.next();
                    return new MalList(new MalSymbol("quote"),
                                       read_form(rdr));
                case "`": rdr.next();
                    return new MalList(new MalSymbol("quasiquote"),
                                       read_form(rdr));
...
            

Ruby


...
def read_form(rdr)
    return case rdr.peek
        when ";" then  nil
        when "'" then  rdr.next; List.new [:quote, read_form(rdr)]
        when "`" then  rdr.next; List.new [:quasiquote, read_form(rdr)]
        when "~" then  rdr.next; List.new [:unquote, read_form(rdr)]
        when "~@" then rdr.next; List.new [:"splice-unquote", read_form(rdr)]
        when "^" then  rdr.next; meta = read_form(rdr);
                       List.new [:"with-meta", read_form(rdr), meta]
...
            

Perl


...
sub read_form {
    my($rdr) = @_;
    my $token = $rdr->peek();
    given ($token) {
        when("'") { $rdr->next(); List->new([Symbol->new('quote'),
                                             read_form($rdr)]) }
        when('`') { $rdr->next(); List->new([Symbol->new('quasiquote'),
                                             read_form($rdr)]) }
        when('~') { $rdr->next(); List->new([Symbol->new('unquote'),
                                             read_form($rdr)]) }
...
            

Go


...
func read_form(rdr Reader) (MalType, error) {
        token := rdr.peek()
        if token == nil {
                return nil, errors.New("read_form underflow")
        }
        switch *token {
        case `'`:
                rdr.next()
                form, e := read_form(rdr)
                if e != nil {
                        return nil, e
                }
                return List{[]MalType{Symbol{"quote"}, form}, nil}, nil
...
            

Rust


...
fn read_form(rdr : &mut Reader) -> MalRet {
    let otoken = rdr.peek();
    let stoken = otoken.unwrap();
    let token = &stoken[..];
    match token {
        "'" => {
            let _ = rdr.next();
            match read_form(rdr) {
                Ok(f) => Ok(list(vec![symbol("quote"), f])),
                Err(e) => Err(e),
            }
        },
...
            

R


...
read_form <- function(rdr) {
    token <- Reader.peek(rdr)
    if (token == "'") {
        . <- Reader.next(rdr);
        new.list(new.symbol("quote"), read_form(rdr))
    } else if (token == "`") {
        . <- Reader.next(rdr);
        new.list(new.symbol("quasiquote"), read_form(rdr))
    } else if (token == "~") {
        . <- Reader.next(rdr);
        new.list(new.symbol("unquote"), read_form(rdr))
...
            

CoffeeScript


...
read_form = (rdr) ->
  token = rdr.peek()
  switch token
    when '\'' then [_symbol('quote'), read_form(rdr.skip())]
    when '`'  then [_symbol('quasiquote'), read_form(rdr.skip())]
    when '~'  then [_symbol('unquote'), read_form(rdr.skip())]
    when '~@' then [_symbol('splice-unquote'), read_form(rdr.skip())]
    when '^'
      meta = read_form(rdr.skip())
      [_symbol('with-meta'), read_form(rdr), meta]
    when '@' then [_symbol('deref'), read_form(rdr.skip())]
...
            

VB.NET


...
        Shared Function read_form(rdr As Reader) As MalVal
            Dim token As String = rdr.peek()
            If token Is Nothing Then
                throw New MalContinue()
            End If
            Dim form As MalVal = Nothing

            Select token
            Case "'"
                rdr.get_next()
                return New MalList(New MalSymbol("quote"),
                                   read_form(rdr))
...
            

Scala


...
  def read_form(rdr: Reader): Any = {
    return rdr.peek() match {
      case "'"  => { rdr.next; _list(Symbol("quote"), read_form(rdr)) }
      case "`"  => { rdr.next; _list(Symbol("quasiquote"), read_form(rdr)) }
      case "~"  => { rdr.next; _list(Symbol("unquote"), read_form(rdr)) }
      case "~@" => { rdr.next; _list(Symbol("splice-unquote"), read_form(rdr)) }
      case "^"  => { rdr.next; val meta = read_form(rdr);
                     _list(Symbol("with-meta"), read_form(rdr), meta) }
      case "@"  => { rdr.next; _list(Symbol("deref"), read_form(rdr)) }
...
            

Haskell


...
read_form :: Parser MalVal
read_form =  do
    ignored
    x <- read_macro
     <|> read_list
     <|> read_vector
     <|> read_hash_map
     <|> read_atom
    return $ x

read_str :: String -> IOThrows MalVal
read_str str = case parse read_form "Mal" str of
...
            

Racket


...
(define (read_form rdr)
  (let ([token (send rdr peek)])
    (if (null? token)
      (raise (make-blank-exn "blank line" (current-continuation-marks)))
      (cond
        [(equal? "'" token) (send rdr next) (list 'quote (read_form rdr))]
        [(equal? "`" token) (send rdr next) (list 'quasiquote (read_form rdr))]
        [(equal? "~" token) (send rdr next) (list 'unquote (read_form rdr))]
        [(equal? "~@" token) (send rdr next) (list 'splice-unquote (read_form rdr))]
        [(equal? "^" token) (send rdr next)
                            (let ([meta (read_form rdr)])
                              (list 'with-meta (read_form rdr) meta))]
...
            

Lua


...
function M.read_form(rdr)
    local token = rdr:peek()

    if "'" == token then
        rdr:next()
        return List:new({Symbol:new('quote'), M.read_form(rdr)})
    elseif '`' == token then
        rdr:next()
        return List:new({Symbol:new('quasiquote'), M.read_form(rdr)})
    elseif '~' == token then
        rdr:next()
        return List:new({Symbol:new('unquote'), M.read_form(rdr)})
...
            

OCaml


...
and read_form all_tokens =
  match all_tokens with
    | [] -> raise End_of_file;
    | token :: tokens ->
      match token with
        | "'"  -> read_quote "quote" tokens
        | "`"  -> read_quote "quasiquote" tokens
        | "~"  -> read_quote "unquote" tokens
        | "~@" -> read_quote "splice-unquote" tokens
        | "@"  -> read_quote "deref" tokens
...
            

Mal Today

Putting the Fun in Language Learning

Buzzword alert: Gamification

Make-A-Lisp is gamification of language learning

Game of Work

  1. Goals
  2. Scorekeeping
  3. Choice
  4. Coaching
  5. Feedback

Goals

  • Learn a New Language
  • Learn About Lisp
  • ---
  • Create a Lisp (i.e. Mal implementation)
  • Get Eternal Glory (at https://github.com/kanaka/mal)
    • Getting your implementation into the main repository
      • First to implement a new target
      • Something unique and interesting about your implemenation
    • Getting credit for a re-implementation

Scorekeeping

Measure of progress towards goal

  • step0_repl
  • step1_read_print
  • step2_eval
  • step3_env
  • step4_if_fn_do
  • step5_tco
  • step6_file
  • step7_quote
  • step8_macros
  • step9_try
  • stepA_mal
  • Echo program
  • Syntax checker
  • Simple calculator (prefix)
  • Calculator with memory
  • Simple Lisp Language
  • Efficient stack/memory (Tail-calls)
  • File I/O, eval, command line
  • Code templating (quasiquote)
  • User defined syntax (macros)
  • Exception handling
  • Self-hosting

Choice

Freedom to choose how to succeed

  • Target language and tools
  • Implementation decisions
  • Optional steps/tasks
    • step 5 / TCO
    • readline editing/history
    • metadata across all compound data-types
    • keywords
  • Deferrable (needed for self-hosting):
    • step 5 / TCO
    • vectors and hash-maps
    • reader macros, comments

Coaching

Feedback

  • Measuring change over time
  • Frequent feedback
  • In make-a-lisp feedback works like this:
    
    make test^MY_IMPL
    
  • Or:
    
    make test^MY_IMPL^stepX
    

Game Overview (Turns)

  • For each step:
    1. Choose a step task
    2. Google, stackoverflow, references, tutorials
    3. Implement/tweak until tests pass
    4. Repeat until step is done

Let's Play

  • Round one

Your Turn!

  • Learn a new language using mal
  • Create an new implementation
  • Improve the game
  • Improve an existing implementation
  • Make a cool logo for mal
  • Do some language research with mal implementations

Questions?

Extra Material

New Implementation Ideas

  • Something old
    • Fortran
    • COBOL
    • Pascal
    • Ada
    • Assembly
  • Something new
    • Idris
    • Io
    • Dart
    • Elm
    • TypeScript


  • Something borrowed
    (from another domain)
    • TeX
    • PL/SQL
    • Prolog
    • Verilog / VHDL
  • Something blue
    (corporate/government)
    • Objective-C
    • PowerShell
    • ColdFusion
    • MUMPS (OpenM)