All Articles

The Number Guessing Game Written in YAML as LISP Interpreted with Python

I like the number guessing game, conceptually. Using this as a first example is especially rewarding in Python since it imposes so few syntax restrictions on its users.

When an absolute beginner looks at a piece of source code, no matter how simple, this is the perception:

The Matrix

Like the incomprehensible green letters in the Matrix, the source code with all its special characters like colons and parentheses looks like it came from another universe.

So, I tried to develop an even more straightforward way to introduce programming. Get rid of the data types, parenthesis and all this annoying stuff. Of course, I did not write a new language - and even if I did, it would not be very useful for everything else except teaching.

An often overused analogy to programming is that of a cooking recipe. And what does a recipe look like? Lots of bullet points, each indicating a step to perform. Sometimes, these bullet points are even nested, like so:

  • mix ingredients a and b

    • add ingredient c to the mix

Doesn’t that look like YAML? Oh, I hear you! YAML is not a programming language, and, even more important: It is certainly not a good choice if one wants to get rid of complex syntax rules. However, I decided to give it a try. What if our number guessing game could look like this:

- repeat:
  - 3
  - what:
    - say:
      - what: Please guess a number
    - store:
      - what: input
      - to: number
    - ifeq:
      - val1:
        - get_store:
          - from: number
      - val2: 777
      - then:
        - say:
          - what: That was correct!
        - break
      - else:
        - say:
          - what: Try again!

That’s YAML, but it looks like LISP. I created a small Python parse for these programmes, and it turned out to work.

Here is the Parser code.

import sys
from yaml import load
try:
    from yaml import CLoader as Loader
except ImportError:
    from yaml import Loader


class ControlException(Exception): pass
class BreakException(ControlException): pass


class Parser:
    def __init__(self, data):
        """ Constructor takes the parsed yaml file
        and initializes the variable storage."""
        self.data = data    # parsed yaml
        self._store = {}    # the variable store

    def run(self):
        """This function is the entry point for the interpreter."""
        for line in self.data:
            self.exec_line(line)
    
    def exec_line(self, line):
        # If the line is a string, it could still be an exposed function,
        # so call that.
        if type(line) == str or type(line) == int:
            return self.str_or_func(line)
        name = list(line.keys())[0]
        fn = self.__getattribute__("exp_"+name)
        return fn(line[name])

    def str_or_func(self, x):
        """Returns the result of an exposed function,
        or the string otherwise."""
        try:
            fn = self.__getattribute__("exp_"+x)
            return fn()
        except Exception as e:
            # Our own ControlExceptions must be re-raised.
            if isinstance(e, ControlException):
                raise e
            return x

    def car(self, arg):
        if type(arg) == dict:
            return self.exec_line(arg)
        if type(arg) == list:
            res = [self.exec_line(code) for code in arg]
            return res[0] if len(res) == 1 else res
        else:
            res = arg
        return self.exec_line(res)

    exp_from = exp_else = exp_then = exp_val1 = exp_val2 = exp_what = car

    def exp_break(self):
        raise BreakException

    def exp_get_store(self, arg):
        fr = arg[0]
        s = self.exp_what(fr)
        try:
            return self._store.get(s)
        except:
            print("ERR", s)

    def exp_store(self, arg):
        kwargs = (arg[0] | arg[1])
        self._store[kwargs['to']] = self.exp_what(kwargs['what'])
        # No return here, means, we have a statement, not an expression ;)

    def exp_input(self):
        return input("Please enter some value:")

    def exp_say(self, *args):
        for arg in args:
            for code in arg:
                what = self.exec_line(code)
        print(what)

    def exp_ifeq(self, arg):
        val1 = self.exec_line(arg[0])
        val2 = self.exec_line(arg[1])
        if str(val1) == str(val2):
            then = self.exec_line(arg[2])
            return then
        else:
            try:
                otherwise = self.exec_line(arg[3])
            except IndexError:
                pass
            except:
                raise
    
    def exp_repeat(self, arg):
        n = arg[0]
        for code in arg[1:]:
            for i in range(int(n)):
                try:
                    self.exec_line(code)
                except BreakException:
                    break

    def exp_concat(self, arg):
        s  = ""
        for code in arg:
            s += str(self.exp_what(code))
        return s

    def exp_plus(self, arg):
        return sum(int(self.exp_what(code)) for code in arg)



if __name__=="__main__":
    data = load(open(sys.argv[1], 'r'), Loader=Loader)
    parser = Parser(data)
    parser.run()

Certainly not a production-ready LISP parser, but definitely enough to make the point in teaching.

Whenever a key in the YAML file has a corresponding exp_<key> callable in the parser class, this callable will then be invoked. The way we make these pseudo keyword arguments is by simply aliasing the names of these keywords to our car function.

Why car?

car and cdr are primitive operations of the LISP programming language. car returns the first element of a list, while cdr returns the rest. In Python, this would look like this:

car = the_list[0]
cdr = the_list[1:]

In essence, this is what my implementation of car does. It evaluates the list. Of course, there are some conceptual differences, such as evaluating all the way down recursively, but I still see it as a homage to LISP in that case.

I still need to try this YAML-LISP Number guessing game in my courses, but what do you think: Would the YAML programme make more sense to programming newbies, or does it lead to even more confusion? Of course, I would not cover the mechanics of the parser/interpreter as such in beginner’s courses.