{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 1. Implementing Scheme in Python" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.1 Complete with Continuations, Nondeterminsitic search, and No Stack Limits" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook is a summary of the explorations in **CS245: Programming Languages** at [Bryn Mawr College](http://brynmawr.edu/) [Department of Computer Science](http://cs.brynmawr.edu/), by [Douglas Blank](http://cs.brynmawr.edu/~dblank/). If you have questions or comments, please use the comments at the bottom of this notebook.\n", "\n", "This notebook describes the basic techniques of implementing Scheme in Python in Continuation-Passing Style (CPS), and call-with-current-continuation. A more rich implementation of Scheme-in-Python is [Calysto Scheme](https://github.com/Calysto/calysto_scheme). You can read some documention about it [here](http://calicoproject.org/Calico_Scheme), and see a debugging video [here](https://www.youtube.com/watch?v=2w-iO701g_w).\n", "\n", "**Goal**: explore implementing the core of a real Scheme in Python in 50 cells. \n", "\n", "First, we need to write some parsing code so that Python will be able to read Scheme's [symbolic expressions](https://en.wikipedia.org/wiki/S-expression), also called \"s-expressions\".\n", "\n", "We start at the lowest level, taking a string and return a list of strings broken up based on white space and brackets." ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from __future__ import print_function\n", "\n", "def lexer(string):\n", " retval = []\n", " current = \"\"\n", " for i in range(len(string)):\n", " if string[i] in [\"(\", \"[\", \")\", \"]\"]:\n", " if current:\n", " retval.append(current)\n", " current = \"\"\n", " retval.append(string[i])\n", " elif string[i] in [\" \", \"\\t\", \"\\n\"]:\n", " if current:\n", " retval.append(current)\n", " current = \"\"\n", " else:\n", " current += string[i]\n", " if current:\n", " retval.append(current)\n", " return retval" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['(', 'this', 'is', 'a', '(', 'test', ')', 'of', '1', '[', 'thing', ']', ')']" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lexer(\"(this is a (test) of 1 [thing])\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The output of `lexer` is just a list of parts. We now pass that to `reader` which will create lists, and turn strings of numbers into actual numbers: " ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def reader(lexp):\n", " current = None\n", " stack = []\n", " for item in lexp:\n", " if item.isdigit():\n", " if current is not None:\n", " current.append(eval(item))\n", " else:\n", " current = eval(item)\n", " elif item in [\"[\", \"(\"]:\n", " if current is not None:\n", " stack.append(current)\n", " current = []\n", " elif item in [\"]\", \")\"]:\n", " if stack:\n", " stack[-1].append(current)\n", " current = stack.pop(-1)\n", " else:\n", " pass\n", " else:\n", " if current is not None:\n", " current.append(item)\n", " else:\n", " current = item\n", " return current" ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['this', 'is', 'a', ['test'], 'of', 1, 'or', 2, ['things']]" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "reader(lexer(\"(this is a (test) of 1 or 2 [things])\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we wanted, we could also parse quoted strings and floating-point numbers in `reader`. We'll add those later." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we are ready to construct s-expressions. Our goal is to feed the output of reader into parser, and have the parser create [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). \n", "\n", "First, we define in Python all of the functions and classes that we will need to process s-expressions, including: `car`, `cdr`, and `cons`:" ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class Symbol(str):\n", " \"Class to define symbols, which should be unique items\"\n", " def __repr__(self):\n", " return str.__repr__(self)[1:-1] # don't show quotation marks\n", " \n", "EmptyList = Symbol(\"()\")\n", "void = Symbol(\"\")\n", " \n", "class Cons(object):\n", " \"A cell/link used to construct linked-list\"\n", " def __init__(self, car, cdr):\n", " self.car = car\n", " self.cdr = cdr\n", " def __repr__(self):\n", " if self.car == \"closure-exp\":\n", " return \"#\"\n", " retval = \"\"\n", " current = self\n", " while current is not EmptyList and isinstance(current, Cons):\n", " if retval != \"\":\n", " retval += \" \"\n", " retval += str(current.car) # recursion here!\n", " current = current.cdr\n", " if current is not EmptyList:\n", " retval += \" . \" + str(current)\n", " return \"(%s)\" % retval\n", " \n", "class String(str):\n", " \"Class to wrap strings so that we can define repr\"\n", " def __repr__(self):\n", " return '\"%s\"' % str.__repr__(self)[1:-1]\n", " def __str__(self):\n", " return self.__repr__()\n", "\n", "def List(*args):\n", " \"Create a linked-list of items\"\n", " retval = EmptyList\n", " for arg in reversed(args):\n", " retval = cons(arg, retval)\n", " return retval\n", " \n", "def car(exp):\n", " return exp.car\n", "def cadr(exp):\n", " return exp.cdr.car\n", "def caddr(exp):\n", " return exp.cdr.cdr.car\n", "def cadddr(exp):\n", " return exp.cdr.cdr.cdr.car\n", "\n", "def cdr(exp):\n", " return exp.cdr\n", "def cddr(exp):\n", " return exp.cdr.cdr\n", "def cdddr(exp):\n", " return exp.cdr.cdr.cdr\n", "def cddddr(exp):\n", " return exp.cdr.cdr.cdr.cdr\n", "\n", "def cons(item1, item2):\n", " return Cons(item1, item2)\n", "\n", "def length(item, handler):\n", " current = item\n", " count = 0\n", " while current is not EmptyList and isinstance(current, Cons):\n", " current = current.cdr\n", " count += 1\n", " if current is not EmptyList:\n", " return handler(\"Attempt to take length of improper list\")\n", " return count\n", "\n", "def rac(item):\n", " current = item\n", " previous = None\n", " while current is not EmptyList:\n", " previous = current.car\n", " current = current.cdr\n", " return previous\n", "\n", "## We use \"_q\" in Python to represent \"?\" in Scheme.\n", "\n", "def pair_q(item):\n", " return isinstance(item, Cons)\n", "\n", "def null_q(lst):\n", " return lst is EmptyList" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that we create classes for Symbols and Cons cells. We use the Symbol class to both make unique items, and to be able to represent Symbols as we wish. We use the Cons class to create linked-lists made of cons cells, and to be able to format these as Scheme lists.\n", "\n", "Now, we construct a set of tagged-lists so that the parser can identify each of the \"special forms\"." ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def lit_exp(value):\n", " return List(\"lit-exp\", value)\n", "\n", "def if_exp(test_exp, then_exp, else_exp):\n", " return List(\"if-exp\", test_exp, then_exp, else_exp)\n", "\n", "def let_exp(variables, values, body):\n", " return List(\"let-exp\", variables, values, body)\n", "\n", "def var_exp(symbol):\n", " return List(\"var-exp\", symbol)\n", "\n", "def assign_exp(symbol, value):\n", " return List(\"assign-exp\", symbol, value)\n", "\n", "def define_exp(symbol, value):\n", " return List(\"define-exp\", symbol, value)\n", "\n", "def app_exp(f, args):\n", " return List(\"app-exp\", f, args)\n", "\n", "def procedure_exp(variables, body):\n", " return List(\"procedure-exp\", variables, body)\n", "\n", "def closure_exp(env, variables, body):\n", " return List(\"closure-exp\", env, variables, body)\n", "\n", "def begin_exp(exps):\n", " return List(\"begin-exp\", exps)\n", "\n", "## and a utility function for defining closure?:\n", "def closure_q(item):\n", " return pair_q(item) and car(item) == \"closure-exp\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And now the parser:" ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def sexp(item):\n", " \"\"\"\n", " Takes an Python list of items and returns Scheme s-exp.\n", " \"\"\"\n", " if isinstance(item, list):\n", " return List(*map(sexp, item)) # recursion!\n", " else:\n", " return item\n", "\n", "def parser(pyexp):\n", " \"\"\"\n", " Reads in a Python list of things, and returns a sexp.\n", " Uses Python stack for recursion.\n", " \"\"\"\n", " if isinstance(pyexp, int):\n", " return lit_exp(pyexp)\n", " elif isinstance(pyexp, str):\n", " return var_exp(pyexp)\n", " elif pyexp[0] == \"quote\":\n", " return lit_exp(sexp(pyexp[1]))\n", " elif pyexp[0] == \"set!\":\n", " return assign_exp(pyexp[1], parser(pyexp[2]))\n", " elif pyexp[0] == \"define\":\n", " return define_exp(pyexp[1], parser(pyexp[2]))\n", " elif pyexp[0] == \"begin\":\n", " return begin_exp(List(*map(parser, pyexp[1:])))\n", " elif pyexp[0] == \"if\":\n", " if len(pyexp) == 3:\n", " return if_exp(parser(pyexp[1]), parser(pyexp[2]), lit_exp(EmptyList))\n", " else:\n", " return if_exp(parser(pyexp[1]), parser(pyexp[2]), parser(pyexp[3]))\n", " elif pyexp[0] == \"let\":\n", " return let_exp(List(*[pair[0] for pair in pyexp[1]]), \n", " List(*map(parser, [pair[1] for pair in pyexp[1]])), \n", " parser(pyexp[2]))\n", " elif pyexp[0] == \"lambda\":\n", " return procedure_exp(List(*pyexp[1]), parser(pyexp[2]))\n", " else:\n", " return app_exp(parser(pyexp[0]), List(*map(parser, pyexp[1:])))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The parser is so short! That is mostly because of Scheme's prefix notation. \n", "\n", "Let's try parsing a few expressions:" ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(let-exp (x y) ((lit-exp 1) (lit-exp 2)) (var-exp x))" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "parser(reader(lexer(\"(let ((x 1)(y 2)) x)\")))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, let's add the pieces necessary to build an interpreter.\n", "\n", "We need to be able to represent boolean values. For now, we will use 0 to represent #f (False). Anything else is #t (True). Note that this is different from Python's idea of true/false. For example, in Python [] and (,) are considered false." ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def true_q(value):\n", " if value == 0:\n", " return False\n", " else:\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "An environment will be a Python list of [variable, value] pairs. Later we can add [Lexial Address](https://en.wikipedia.org/wiki/Scope_(computer_science%29) to make lookup [O(1)](https://en.wikipedia.org/wiki/Big_O_notation).\n", "\n", "When we extend an environment (when we evaluate a `let` or `lambda`) we will make a new \"frame\"... a list of variable/value pairs append onto the front of an existing environment:" ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def list_to_vector(lst):\n", " retval = []\n", " current = lst\n", " while current is not EmptyList:\n", " retval.append(current.car)\n", " current = current.cdr\n", " return retval\n", "\n", "def extend_env(vars, vals, env):\n", " return [list(map(list,zip(list_to_vector(vars), \n", " list_to_vector(vals)))), env]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we look up a variable, we will actually return the [variable, value] pair (or return False):" ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def lookup_binding(symbol, env, first_frame_only=False):\n", " frame = env[0]\n", " for pair in frame:\n", " if pair[0] == symbol:\n", " return pair\n", " if not first_frame_only and len(env) > 1:\n", " return lookup_binding(symbol, env[1])\n", " else:\n", " return False" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And now we are ready to define the interpreter. \n", "\n", "We write the interpreter, here called `evaluator`, in [Continuation Passing Style](https://en.wikipedia.org/wiki/Continuation-passing_style), or CPS. This version is still recursive, but this won't have any impact on our Scheme recursive programs. Our Scheme functions won't use Python's stack---eventually.\n", "\n", "First, we wrote the evaluator in regular form. Then we converted it to CPS. Note that:\n", "\n", "1. We use a third and fourth parameter to evaluator (handler and k) that represent the **error and computation continuations**, respectively\n", "1. We use **Python functions** (closures) to **represent continuations**\n", "1. We pass **results** to the continuation via **apply**\n", "1. Any place where there is still computation to be done, we construct a **new continuation** using Python's **lambda**\n", "\n", "To see more examples of converting Python code into Continuation Passing Style, see:\n", "\n", "* [Review, Continuations and CPS](http://jupyter.cs.brynmawr.edu/hub/dblank/public/Review,%20Continuations%20and%20CPS.ipynb)\n", "* [Assignment05](http://jupyter.cs.brynmawr.edu/hub/dblank/public/Assignment05.ipynb)" ] }, { "cell_type": "code", "execution_count": 52, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def apply(f, *args):\n", " return f(*args)" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def evaluator(exp, env, handler, k):\n", " if car(exp) == \"lit-exp\":\n", " return apply(k, cadr(exp))\n", " elif car(exp) == \"var-exp\":\n", " return apply(k, lookup_binding(cadr(exp), env)[1])\n", " elif car(exp) == \"let-exp\":\n", " variables = cadr(exp)\n", " return apply(evaluator_map, caddr(exp), env, handler,\n", " lambda values: evaluator(cadddr(exp), \n", " extend_env(variables, values, env), handler, k))\n", " elif car(exp) == \"if-exp\":\n", " return apply(evaluator, cadr(exp), env, handler,\n", " lambda value: eval_if(value, exp, env, handler, k))\n", " elif car(exp) == \"assign-exp\":\n", " variable = cadr(exp)\n", " return apply(evaluator, caddr(exp), env, handler,\n", " lambda value: eval_assign(value, variable, env, handler, k))\n", " elif car(exp) == \"define-exp\":\n", " variable = cadr(exp)\n", " return apply(evaluator, caddr(exp), env, handler,\n", " lambda value: eval_define(value, variable, env, handler, k))\n", " elif car(exp) == \"procedure-exp\":\n", " return apply(k, closure_exp(env, cadr(exp), caddr(exp)))\n", " elif car(exp) == \"app-exp\":\n", " return apply(evaluator, cadr(exp), env, handler,\n", " lambda v: evaluator_map(caddr(exp), env, handler,\n", " lambda all: eval_app(v, all, env, handler, k)))\n", " elif car(exp) == \"begin-exp\":\n", " return apply(evaluator_map, cadr(exp), env, handler,\n", " lambda all: apply(k, rac(all)))\n", " else:\n", " return handler(\"invalid abstract syntax: %s\" % exp)\n", "\n", " \n", "def eval_assign(value, variable, env, handler, k):\n", " binding = lookup_binding(variable, env)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " return handler(\"No such variable: '%s'\" % variable)\n", " return apply(k, value)\n", "\n", "def eval_define(value, variable, env, handler, k):\n", " binding = lookup_binding(variable, env, True)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " env[0].append([variable, value])\n", " return apply(k, None) \n", " \n", "def eval_if(value, exp, env, handler, k):\n", " if true_q(value):\n", " return apply(evaluator, caddr(exp), env, handler, k)\n", " else:\n", " return apply(evaluator, cadddr(exp), env, handler, k)\n", "\n", "def evaluator_map(exp, env, handler, k):\n", " if null_q(exp):\n", " return apply(k, EmptyList)\n", " else:\n", " return apply(evaluator, car(exp), env, handler,\n", " lambda v1: evaluator_map(cdr(exp), env, handler,\n", " lambda rest: apply(k, cons(v1, rest))))\n", " \n", "def eval_app(f, args, env, handler, k):\n", " if closure_q(f):\n", " env = cadr(f)\n", " parameters = caddr(f)\n", " body = cadddr(f)\n", " return apply(evaluator, body, extend_env(parameters, args, env), handler, k)\n", " else: \n", " return f(args, env, handler, k)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we construct a toplevel environment with some primitives, like +, -, \\*, print, etc. Primitives are written in CPS." ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import operator\n", "\n", "def add_prim(args, env, handler, k):\n", " return apply(k, operator.add(*list_to_vector(args)))\n", "\n", "def sub_prim(args, env, handler, k):\n", " return apply(k, operator.sub(*list_to_vector(args)))\n", "\n", "def mul_prim(args, env, handler, k):\n", " return apply(k, operator.mul(*list_to_vector(args)))\n", "\n", "def div_prim(args, env, handler, k):\n", " return apply(k, operator.div(*list_to_vector(args)))\n", "\n", "def equal_sign_prim(args, env, handler, k):\n", " a = car(args)\n", " b = cadr(args)\n", " return apply(k, 1 if a == b else 0)\n", "\n", "def less_than_prim(args, env, handler, k):\n", " a = car(args)\n", " b = cadr(args)\n", " return apply(k, 1 if a < b else 0)\n", "\n", "def cons_prim(args, env, handler, k):\n", " return apply(k, cons(car(args), cadr(args)))\n", "\n", "def print_prim(args, env, handler, k):\n", " print(*list_to_vector(args))\n", " return apply(k, void)\n", " \n", "def input_prim(args, env, handler, k):\n", " prompt = \"\"\n", " if length(args, handler) > 0:\n", " prompt = car(args)\n", " retval = raw_input(prompt)\n", " return apply(k, retval)\n", " \n", "def length_prim(args, env, handler, k):\n", " return apply(k, length(car(args), handler))\n", "\n", "def list_prim(args, env, handler, k):\n", " return apply(k, args)\n", "\n", "def not_prim(args, env, handler, k):\n", " return apply(k, 0 if true_q(car(args)) else 1)\n", "\n", "def pair_q_prim(args, env, handler, k):\n", " return apply(k, 1 if pair_q(car(args)) else 0)\n", "\n", "def error_prim(args, env, handler, k):\n", " return apply(handler, car(args))\n", "\n", "def null_q_prim(args, env, handler, k):\n", " return apply(k, 1 if null_q(car(args)) else 0)\n", "\n", "def car_prim(args, env, handler, k):\n", " return apply(k, car(car(args)))\n", "\n", "def cdr_prim(args, env, handler, k):\n", " return apply(k, cdr(car(args)))\n", "\n", "def map_prim(args, env, handler, k):\n", " f = car(args)\n", " args = cadr(args)\n", " if null_q(args):\n", " return apply(k, args)\n", " else:\n", " return eval_app(f, args, env, handler,\n", " lambda v: map_prim(List(f, cdr(args)), env, handler, \n", " lambda rest: apply(k, cons(v, rest))))\n", "\n", "def for_each_prim(args, env, handler, k):\n", " f = car(args)\n", " args = cadr(args)\n", " if null_q(args):\n", " return apply(k, args)\n", " else:\n", " return eval_app(f, args, env, handler,\n", " lambda v: map_prim(List(f, cdr(args)), env, handler, \n", " lambda rest: apply(k, cons(v, rest))))\n", "\n", "def callcc_prim(args, env, handler, k):\n", " proc = car(args)\n", " fake_k = lambda args, env, handler, k2: apply(k, car(args))\n", " return eval_app(proc, List(fake_k), env, handler, k)\n", "\n", "toplevel_env = [[[\"()\", EmptyList],\n", " [\"not\", not_prim],\n", " [\"pair?\", pair_q_prim],\n", " [\"error\", error_prim],\n", " [\"null?\", null_q_prim],\n", " [\"map\", map_prim],\n", " [\"car\", car_prim],\n", " [\"cdr\", cdr_prim],\n", " [\"+\", add_prim], \n", " [\"-\", sub_prim],\n", " [\"*\", mul_prim],\n", " [\"/\", div_prim],\n", " [\"=\", equal_sign_prim],\n", " [\"<\", less_than_prim],\n", " [\"cons\", cons_prim],\n", " [\"print\", print_prim],\n", " [\"input\", input_prim],\n", " [\"length\", length_prim],\n", " [\"list\", list_prim],\n", " [\"call/cc\", callcc_prim]]] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that all of the primitives are defined using CPS as well. That allows us to implement exotic functions, such as `call-with-current-continuation` (call/cc) as shown." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we define a short-cut for running programs:" ] }, { "cell_type": "code", "execution_count": 55, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def scheme(exp):\n", " return evaluator(parser(reader(lexer(exp))), \n", " toplevel_env, \n", " lambda error: error, \n", " lambda v: v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And now, we are ready to compute!" ] }, { "cell_type": "code", "execution_count": 56, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(1 . 2)" ] }, "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(cons 1 2)\")" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(+ 1 2)\")" ] }, { "cell_type": "code", "execution_count": 58, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(length (list 1 2))\")" ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "6" ] }, "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(if (+ 1 2) 6 7)\")" ] }, { "cell_type": "code", "execution_count": 60, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "43" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"((lambda (n) (+ n 1)) 42)\")" ] }, { "cell_type": "code", "execution_count": 61, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(if 0 2 3)\")" ] }, { "cell_type": "code", "execution_count": 62, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(if 1 2 3)\")" ] }, { "cell_type": "code", "execution_count": 63, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "23" ] }, "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(+ 1 (error 23))\")" ] }, { "cell_type": "code", "execution_count": 64, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "12 56\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(print 12 56)\")" ] }, { "cell_type": "code", "execution_count": 65, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 3\n" ] }, { "data": { "text/plain": [ "4" ] }, "execution_count": 65, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(begin (print 1 3) (+ 2 3) (+ 3 3) 4)\")" ] }, { "cell_type": "code", "execution_count": 66, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "3\n", "4\n" ] }, { "data": { "text/plain": [ "( )" ] }, "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(map (lambda (n) (print n)) (quote (1 2 3 4)))\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.2 Python's Stack" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can define global recursive functions:" ] }, { "cell_type": "code", "execution_count": 67, "metadata": { "collapsed": false }, "outputs": [], "source": [ "scheme(\"(define factorial (lambda (n) (if (= n 1) 1 (* n (factorial (- n 1))))))\")" ] }, { "cell_type": "code", "execution_count": 68, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "#" ] }, "execution_count": 68, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"factorial\")" ] }, { "cell_type": "code", "execution_count": 69, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(factorial 1)\")" ] }, { "cell_type": "code", "execution_count": 70, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "120" ] }, "execution_count": 70, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(factorial 5)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Unfortunately, we are still using Python's stack. So this fails:" ] }, { "cell_type": "code", "execution_count": 71, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Crashed Python's stack!\n" ] } ], "source": [ "try:\n", " scheme(\"(factorial 1000)\")\n", "except:\n", " print(\"Crashed Python's stack!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.5 Getting rid of Python's stack" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It turns out to be very easy to stop using Python's stack for our function calls. We will delay executing continuations. So, instead of apply k to v, instead we just package it up.\n", "\n", "Recall how we have defined these functions:\n", "\n", "```python\n", "def apply(f, *args):\n", " return f(*args)\n", " \n", "def scheme(exp):\n", " return evaluator(parser(reader(lexer(exp))), \n", " toplevel_env, \n", " lambda error: error, \n", " lambda v: v)\n", "```\n", "\n", "We then implement a very simple trampoline that will execute the continuation. This won't crash Python's stask because we execute each continuation outside of the recursive calls:" ] }, { "cell_type": "code", "execution_count": 72, "metadata": { "collapsed": true }, "outputs": [], "source": [ "## Use Python's list as continuation wrapper:\n", "\n", "def apply(k, *v):\n", " return [\"\", k, v]\n", "\n", "def continuation_q(exp):\n", " return (isinstance(exp, list) and \n", " len(exp) > 0 and\n", " exp[0] == \"\")\n", "\n", "def trampoline(result):\n", " while continuation_q(result):\n", " f = result[1]\n", " args = result[2]\n", " result = f(*args)\n", " return result\n", "\n", "def scheme(exp):\n", " return trampoline(evaluator(parser(reader(lexer(exp))), toplevel_env, lambda e: e, lambda v: v))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we can evaluate much deeper recursive calls than Python:" ] }, { "cell_type": "code", "execution_count": 73, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "402387260077093773543702433923003985719374864210714632543799910429938512398629020592044208486969404800479988610197196058631666872994808558901323829669944590997424504087073759918823627727188732519779505950995276120874975462497043601418278094646496291056393887437886487337119181045825783647849977012476632889835955735432513185323958463075557409114262417474349347553428646576611667797396668820291207379143853719588249808126867838374559731746136085379534524221586593201928090878297308431392844403281231558611036976801357304216168747609675871348312025478589320767169132448426236131412508780208000261683151027341827977704784635868170164365024153691398281264810213092761244896359928705114964975419909342221566832572080821333186116811553615836546984046708975602900950537616475847728421889679646244945160765353408198901385442487984959953319101723355556602139450399736280750137837615307127761926849034352625200015888535147331611702103968175921510907788019393178114194545257223865541461062892187960223838971476088506276862967146674697562911234082439208160153780889893964518263243671616762179168909779911903754031274622289988005195444414282012187361745992642956581746628302955570299024324153181617210465832036786906117260158783520751516284225540265170483304226143974286933061690897968482590125458327168226458066526769958652682272807075781391858178889652208164348344825993266043367660176999612831860788386150279465955131156552036093988180612138558600301435694527224206344631797460594682573103790084024432438465657245014402821885252470935190620929023136493273497565513958720559654228749774011413346962715422845862377387538230483865688976461927383814900140767310446640259899490222221765904339901886018566526485061799702356193897017860040811889729918311021171229845901641921068884387121855646124960798722908519296819372388642614839657382291123125024186649353143970137428531926649875337218940694281434118520158014123344828015051399694290153483077644569099073152433278288269864602789864321139083506217095002597389863554277196742822248757586765752344220207573630569498825087968928162753848863396909959826280956121450994871701244516461260379029309120889086942028510640182154399457156805941872748998094254742173582401063677404595741785160829230135358081840096996372524230560855903700624271243416909004153690105933983835777939410970027753472000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ] }, "execution_count": 73, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(factorial 1000)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running Forever" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In fact, this would run forever:\n", "\n", "```python\n", "scheme(\"\"\"\n", "(define loop\n", " (lambda ()\n", " (loop)))\n", "(loop)\n", "\"\"\")\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This loop doesn't use up memory as it runs... once started, it has zero additional impact. Why?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Efficiency" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How do the runtimes compare between Python and Scheme-in-Python?" ] }, { "cell_type": "code", "execution_count": 74, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10 loops, best of 3: 109 ms per loop\n" ] } ], "source": [ "%%timeit\n", "scheme(\"(factorial 900)\")" ] }, { "cell_type": "code", "execution_count": 75, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def fact(n):\n", " if n == 1:\n", " return 1\n", " else:\n", " return n * fact(n - 1)" ] }, { "cell_type": "code", "execution_count": 76, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1000 loops, best of 3: 528 µs per loop\n" ] } ], "source": [ "%%timeit\n", "fact(900)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, the Scheme-in-Python runs about **2 magnitudes slower** than Python for a program that has lots of recursive function calls. That's not good, but there are some things we can do to improve that a bit. But for many programmers, this is a price worth paying for the additional power (e.g., continuations, call/cc, and no stack limit)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.3 Call/cc\n", "\n", "\n", "`call-with-current-continuation` is a function that takes a function (f) and gives it the computation left to do.\n", "\n", "Consider attempting to implement return as in the following example:" ] }, { "cell_type": "code", "execution_count": 80, "metadata": { "collapsed": true }, "outputs": [], "source": [ "scheme(\"\"\"\n", "(define f \n", " (lambda (return)\n", " (begin\n", " (return 2)\n", " 3)))\"\"\")" ] }, { "cell_type": "code", "execution_count": 81, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 81, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"\"\"\n", "(print (f (lambda (x) x))) \n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nothing a regular programming language would allow you to implement getting 2 back from this function.\n", "\n", "But, with call/cc:" ] }, { "cell_type": "code", "execution_count": 82, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"\"\"\n", "(print (call/cc f)) \n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.3.1 Continuations as First Class Objects" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Our Scheme language uses continuations, but we can't really take advantage of them unless we can actually grab them. call-with-current-continuation allows us to grab them.\n", "\n", "We define a function called `current-continuation` that uses call/cc to simply return the current continuation:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Consider the following:\n", "\n", "```\n", "(call/cc (lambda (cc) (cc cc)))\n", "```" ] }, { "cell_type": "code", "execution_count": 86, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "3\n", "4\n", "5\n", "6\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 86, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"\"\"\n", "(begin \n", " (print 1)\n", " (print 2)\n", " (print 3)\n", " (define resume (call/cc (lambda (cc) (cc cc))))\n", " (print 4)\n", " (print 5)\n", " (print 6)\n", ")\n", "\"\"\")" ] }, { "cell_type": "code", "execution_count": 87, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4\n", "5\n", "6\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 87, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"\"\"\n", "(resume resume)\n", "\"\"\")" ] }, { "cell_type": "code", "execution_count": 88, "metadata": { "collapsed": true }, "outputs": [], "source": [ "scheme(\"\"\"\n", "(define current-continuation\n", " (lambda ()\n", " (call/cc (lambda (cc)\n", " (cc cc)))))\n", "\"\"\")" ] }, { "cell_type": "code", "execution_count": 89, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "3\n", "4\n", "5\n", "6\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"\"\"\n", "(begin \n", " (print 1)\n", " (print 2)\n", " (print 3)\n", " (define resume (current-continuation))\n", " (print 4)\n", " (print 5)\n", " (print 6)\n", ")\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But, we can go back in time and run the code starting at the point where we grabbed the continuation:" ] }, { "cell_type": "code", "execution_count": 90, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4\n", "5\n", "6\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 90, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(resume resume)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And do it again and again and ..." ] }, { "cell_type": "code", "execution_count": 91, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4\n", "5\n", "6\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 91, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(resume resume)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.4 Amb - nondeterministic programming" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have call/cc we can implement the [amb operator](https://en.wikipedia.org/wiki/Nondeterministic_programming) (called \"choose\" in Calysto Scheme).\n", "\n", "We will keep track of decision points in a `fail-stack`. Each time we \"fail\", we will go back to that point in the computation and resume computing again, with the next choice." ] }, { "cell_type": "code", "execution_count": 92, "metadata": { "collapsed": false }, "outputs": [], "source": [ "scheme(\"(define fail-stack (quote ()))\")\n", "\n", "scheme(\"\"\"\n", "(define fail\n", " (lambda ()\n", " (if (not (pair? fail-stack))\n", " (error (quote ()))\n", " (begin\n", " (let ((back-track-point (car fail-stack)))\n", " (begin\n", " (set! fail-stack (cdr fail-stack))\n", " (back-track-point back-track-point)))))))\n", "\"\"\")\n", "\n", "scheme(\"\"\"\n", "(define amb \n", " (lambda (choices)\n", " (let ((cc (current-continuation)))\n", " (if (null? choices) \n", " (fail)\n", " (if (pair? choices) \n", " (let ((choice (car choices)))\n", " (begin \n", " (set! choices (cdr choices))\n", " (set! fail-stack (cons cc fail-stack))\n", " choice)))))))\n", "\"\"\")\n", "\n", "scheme(\"\"\"\n", "(define assert \n", " (lambda (condition)\n", " (if (not condition)\n", " (fail)\n", " 1)))\n", "\"\"\")" ] }, { "cell_type": "code", "execution_count": 93, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 93, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(amb (list 1 2 3))\")" ] }, { "cell_type": "code", "execution_count": 94, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 94, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(fail)\")" ] }, { "cell_type": "code", "execution_count": 95, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 95, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(fail)\")" ] }, { "cell_type": "code", "execution_count": 96, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "()" ] }, "execution_count": 96, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"(fail)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pythagorean Mystery" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We're looking for dimensions of a legal right triangle using the Pythagorean theorem:\n", "\n", "$ a^2 + b^2 = c^2 $\n", "\n", "And, we want the second side (b) to be the shorter one:\n", "\n", "$ b < a $\n", "\n", "We can express the problem as three variables and two asserts:" ] }, { "cell_type": "code", "execution_count": 97, "metadata": { "collapsed": true }, "outputs": [], "source": [ "scheme(\"(define fail-stack (quote ()))\")" ] }, { "cell_type": "code", "execution_count": 98, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(4 3 5)\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 98, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scheme(\"\"\"\n", "(let ((a (amb (list 1 2 3 4 5 6 7)))\n", " (b (amb (list 1 2 3 4 5 6 7)))\n", " (c (amb (list 1 2 3 4 5 6 7))))\n", " (begin\n", " (assert (= (* c c) (+ (* a a) (* b b))))\n", " (assert (< b a))\n", " (print (list a b c))\n", " )\n", ")\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.6 Summary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have examined how one could implement the \"hard parts\" of Scheme in Python. But there are still more things to do:\n", "\n", "* Error checking: to make the concepts easy to see, we left out most error checking. We should add that in a manner that doesn't slow down processing too much.\n", "* Add [Lexical Address](https://mitpress.mit.edu/sicp/full-text/sicp/book/node131.html): use indexing to get variable values rather than sequential search.\n", "* Feedback when there is an error: we could easily add stack-like traces to this Scheme that look just like Python's. That would address one of [Guido's concerns](http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html). Read that, compare to this code, and you'll see why Guido is wrong.\n", "* There is still recursion (and thus stack use) in the interpreter: we need to get rid of the evaluator and sexp recursions. We can do that by turning recursive calls into a type of continuation, or rewriting parts in iterative forms. Of course, our Scheme will remain stack-free. \n", "* Macros: we will need to write a `define-syntax`. But that is an independent function performed in the parser.\n", "* Integrate with Python. This is easy: call Python functions, call Scheme functions from Python, import Python modules.\n", "\n", "There are many subtleties left to discover! You may want to start with this code and add to it to get to a complete Scheme implementation. Try the below exercises to get started.\n", "\n", "All of the above (and more!) have been done in [Calysto Scheme](https://github.com/Calysto/calysto/blob/master/calysto/language/scheme.py). However, Calysto Scheme was written in Scheme and converted to Python (similar to the above) via an automated process (well, 1k lines of Python written by hand, and 8k lines of Python generated by [Calico Scheme](https://bitbucket.org/ipre/calico/src/master/languages/Scheme/Scheme/)). You can read more about the project [here](http://calicoproject.org/Calico_Scheme).\n", "\n", "We hope that this was useful!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 2. Exercises" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Try implementing the following (roughly listed from easiest to hardest):\n", "\n", "1. Test all of the special forms. Report any errors to .\n", "1. Add error checking. For example, currently if you attempt to take the car of a non-pair or access undefined variables, you get an ugly error. Use the `handler` to return errors.\n", "1. Add double-quoted strings to the language.\n", "1. Add floating-point numbers to the language.\n", "1. Add missing functions, like: atom?.\n", "1. Add `eq?` that uses \" obj1 is obj2\" to compare objects.\n", "1. Add a Boolean type/class and use it rather than zero/one for True/False. Make it show as #t/#f.\n", "1. Add quote symbol to the parser so that 'item expands to (quote item).\n", "1. Add `trace-lambda` to the language to help with debugging.\n", "1. Add a stack-trace to show \"stack-like\" output when there is an error (eg, keep track of app-exp entrance and exit).\n", "1. Add `cond` special form to the language. You will need `else` (which could be defined to be True).\n", "1. Try some of your homework solutions to see if the version of Scheme in Python can do everything that Calysto Scheme can do. If not, add it!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", " \n", " comments powered by Disqus" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.2" } }, "nbformat": 4, "nbformat_minor": 0 }