diff --git a/ipynb/lispy.ipynb b/ipynb/lispy.ipynb
index 46af1ec..ab2b2a6 100644
--- a/ipynb/lispy.ipynb
+++ b/ipynb/lispy.ipynb
@@ -2,65 +2,77 @@
"cells": [
{
"cell_type": "markdown",
- "id": "61b75f91-0729-4bc0-a107-f77a4a62eb98",
+ "id": "828d21f7-0a3b-4024-b3d5-306ea56a3214",
"metadata": {},
"source": [
"
'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7734c900-7d9c-4770-987a-30830989175d",
+ "metadata": {},
+ "source": [
+ "We see that a procedure has three components: a list of parameter names, a body expression, and an environment that tells us what other variables are accessible from the body. For a procedure defined at the top level this will be the global environment, but it is also possible for a procedure to refer to the local variables of the environment in which it was defined (**not** the environment in which it is called).\n",
"\n",
- "# Now transfer the old global_env into a new Env\n",
- "global_env = Env(global_env)"
+ "The function `find` is used to find the right environment for a variable: starting with the inner one and going out, find the first environment that mentions the variable name."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "22fd9960-b288-4a01-94fe-faab8e8da8bf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def find(var: Symbol, env: Env) -> Env:\n",
+ " \"\"\"Find the environment that contains the variable `var`.\"\"\"\n",
+ " return env if (var in env) else find(var, env['_outer'])"
]
},
{
@@ -466,38 +646,38 @@
"id": "3d0599c6-3a35-4fd8-b179-c970e8a36c4d",
"metadata": {},
"source": [
- "We see that every procedure has three components: a list of parameter names, a body expression, and an environment that tells us what other variables are accessible from the body. For a procedure defined at the top level this will be the global environment, but it is also possible for a procedure to refer to the local variables of the environment in which it was defined (**not** the environment in which it is called).\n",
- "\n",
- "An environment is a subclass of `dict`, so it has all the methods that `dict` has. In addition there are two methods: the constructor `__init__` builds a new environment by taking a list of parameter names and a corresponding list of argument values, and creating a new environment that has those {variable: value} pairs as the inner part, and also refers to the given outer environment. The method `find` is used to find the right environment for a variable: starting with the inner one and going out, find the first environment that mentions the variable name.\n",
- "\n",
- "To see how these all go together, here is the new definition of `eval`. Note that the clause for variable reference has changed: we now have to call `env.find(exp)` to find at what level the variable exists; then we can fetch the value of the variable from that level. (The clause for `define` has not changed, because a define always adds a new variable to the innermost environment.) There are three new clauses: `quote` returns the constant without evaluating it, `set!`, finds the environment level where the variable exists and sets it to a new value there, and for `lambda` we create a new procedure object with the given parameter list, body, and environment."
+ "To see how these all go together, here is the new definition of `eval`:"
]
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 19,
"id": "afa69b35-bac3-459c-a0cc-b94656ab474c",
"metadata": {},
"outputs": [],
"source": [
- "def eval(exp: Exp, env=global_env) -> object:\n",
+ "def eval(exp: Exp, env) -> object:\n",
" \"\"\"Evaluate an expression in an environment.\"\"\"\n",
" match exp:\n",
" case Symbol(): # variable reference\n",
- " return env.find(exp)[exp]\n",
+ " return find(exp, env)[exp]\n",
" case Number(): # constant \n",
" return exp\n",
- " case ('if', test, then, els): # conditional evaluates one branch or the other\n",
- " branch = (then if eval(test, env) \n",
- " else els)\n",
+ " case ('if', test, then_, else_): # conditional evaluates one branch or the other\n",
+ " branch = (else_ if eval(test, env) is False else then_)\n",
" return eval(branch, env)\n",
- " case ('define', name, exp): # definition\n",
- " env[name] = eval(exp, env)\n",
- " case ('quote', constant):\n",
+ " case ('define', (name, *parms), body): # procedure definition\n",
+ " env[name] = eval(['lambda', parms, body], env)\n",
+ " return name\n",
+ " case ('define', Symbol(name), val): # regular definition\n",
+ " env[name] = eval(val, env)\n",
+ " return name\n",
+ " case ('quote', constant): # constant expression\n",
" return constant\n",
- " case ('set!', symbol, exp):\n",
- " env.find(symbol)[symbol] = eval(exp, env)\n",
- " case ('lambda', parms, body):\n",
+ " case ('set!', symbol, val): # variable assignment\n",
+ " find(symbol, env)[symbol] = eval(val, env)\n",
+ " return None\n",
+ " case ('lambda', parms, body): # create an anonymous procedure\n",
" return Procedure(parms, body, env)\n",
" case (proc, *args): # procedure call\n",
" func = eval(proc, env)\n",
@@ -507,20 +687,31 @@
},
{
"cell_type": "markdown",
- "id": "499ec617-e3f5-4f5e-9e8e-e7da7656c624",
+ "id": "73750dec-a8e0-41fb-83ae-3c7740103659",
"metadata": {},
"source": [
- "You can run try new version by removing the `#` and running the cell below:"
+ "For example:"
]
},
{
"cell_type": "code",
- "execution_count": 12,
- "id": "8a76bb61-7195-4b1d-8b01-fb6a5825b94c",
+ "execution_count": 20,
+ "id": "fb398ac0-cd7f-45e2-9a8c-1ea15bc7b4e6",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "314.1592653589793\n"
+ ]
+ }
+ ],
"source": [
- "# repl()"
+ "batch(\"\"\"\n",
+ "(begin \n",
+ " (define (circle-area r) (* pi (* r r)))\n",
+ " (circle-area (+ 5 5)))\"\"\")"
]
},
{
@@ -528,135 +719,138 @@
"id": "1a2ccfcd-326b-44f0-b041-da657dc042a0",
"metadata": {},
"source": [
- "Here is an example of what you can do:\n",
- "\n",
- " >>> repl()\n",
- " \n",
- " lis.py> (define circle-area (lambda (r) (* pi (* r r))))\n",
- " \n",
- " lis.py> (circle-area 3)\n",
- " 28.274333877\n",
- " \n",
- " lis.py> (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))\n",
- " \n",
- " lis.py> (fact 10)\n",
- " 3628800\n",
- " \n",
- " lis.py> (fact 100)\n",
- " 9332621544394415268169923885626670049071596826438162146859296389521759999322991\n",
- " 5608941463976156518286253697920827223758251185210916864000000000000000000000000\n",
- " \n",
- " lis.py> (circle-area (fact 10))\n",
- " 4.1369087198e+13\n",
- " \n",
- " lis.py> (define first car)\n",
- " \n",
- " lis.py> (define rest cdr)\n",
- " \n",
- " lis.py> (define count (lambda (item L) (if L (+ (equal? item (first L)) (count item (rest L))) 0)))\n",
- " \n",
- " lis.py> (count 0 (list 0 1 2 3 0 0))\n",
- " 3\n",
- " \n",
- " lis.py> (count (quote the) (quote (the more the merrier the bigger the better)))\n",
- " 4\n",
- " \n",
- " lis.py> (define twice (lambda (x) (* 2 x)))\n",
- " \n",
- " lis.py> (twice 5)\n",
- " 10\n",
- " \n",
- " lis.py> (define repeat (lambda (f) (lambda (x) (f (f x)))))\n",
- " \n",
- " lis.py> ((repeat twice) 10)\n",
- " 40\n",
- " \n",
- " lis.py> ((repeat (repeat twice)) 10)\n",
- " 160\n",
- " \n",
- " lis.py> ((repeat (repeat (repeat twice))) 10)\n",
- " 2560\n",
- " \n",
- " lis.py> ((repeat (repeat (repeat (repeat twice)))) 10)\n",
- " 655360\n",
- " \n",
- " lis.py> (pow 2 16)\n",
- " 65536.0\n",
- " \n",
- " lis.py> (define fib (lambda (n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2))))))\n",
- " \n",
- " lis.py> (define range (lambda (a b) (if (= a b) (quote ()) (cons a (range (+ a 1) b)))))\n",
- " \n",
- " lis.py> (range 0 10)\n",
- " (0 1 2 3 4 5 6 7 8 9)\n",
- " \n",
- " lis.py> (map fib (range 0 10))\n",
- " (1 1 2 3 5 8 13 21 34 55)\n",
- " \n",
- " lis.py> (map fib (range 0 20))\n",
- " (1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765)\n",
- " \n",
- " \n",
- "We now have a language with procedures, variables, conditionals (`if`), and sequential execution (`begin`). If you are familiar with other languages, you might think that a while or for loop would be needed, but Scheme manages to do without these just fine. The Scheme report says \"Scheme demonstrates that a very small number of rules for forming expressions, with no restrictions on how they are composed, suffice to form a practical and efficient programming language.\" In Scheme you iterate by defining recursive functions.\n",
+ "We now have a language with variables, conditionals, sequential execution, and procedures with recursive calls. That makes our language Turing-complete. If you are familiar with other languages, you might think that a `while` or `for` loop would be needed, but Scheme manages to do without these just fine. In Scheme you iterate by defining recursive functions. The Scheme report says \"Scheme demonstrates that a very small number of rules for forming expressions, with no restrictions on how they are composed, suffice to form a practical and efficient programming language.\" \n",
"\n",
"## How Small/Complete/Good/Fast is Lispy?\n",
" \n",
"In which we judge Lispy on several criteria:\n",
- "- **Small**: Lispy is *very* small: about 120 lines or 4K of source code. (An earlier version was just 90 lines, but was perhaps a bit too terse.) The smallest version of my Scheme in Java, [Jscheme](http://norvig.com/jscheme.html) was 1664 lines and 57K of source. Jscheme was\n",
- " originally called SILK (Scheme in Fifty Kilobytes), but I only kept\n",
- " under that limit by counting bytecode rather than source code. Lispy does much\n",
- " better; I think it meets Alan Kay's 1972 [claim](http://gagne.homedns.org/~tgagne/contrib/EarlyHistoryST.html)\n",
- " that *you could define the \"most powerful language in the world\" in \"a page of code.\"* (if you use a small font). However,\n",
- " I think Alan would disagree, because he would count the Python compiler as part of the code, putting me well over a page.\n",
- "- **Complete**: Lispy is not very complete compared to the Scheme standard. Some major shortcomings:\n",
+ "- **Small**: Lispy is *very* small: about 120 lines or 4K of source code. (An earlier version was just 90 lines, but was perhaps a bit too terse.) The smallest version of my Scheme in Java, [Jscheme](http://norvig.com/jscheme.html) was 1664 lines and 57K of source. Jscheme was originally called SILK (Scheme in Fifty Kilobytes), but I only kept under that limit by counting bytecode rather than source code. Lispy does much better; I think it meets Alan Kay's 1972 [claim](http://gagne.homedns.org/~tgagne/contrib/EarlyHistoryST.html) that *you could define the \"most powerful language in the world\" in \"a page of code.\"* (if you use a small font). However, I think Alan would disagree, because he would count the Python compiler as part of the code, putting me well over a page.\n",
+ "- **Complete**: Lispy is not very complete compared to the [Scheme standard](https://standards.scheme.org/). Some major shortcomings:\n",
" - **Syntax**: Missing comments, quote and quasiquote notation, # literals, the derived\n",
- " expression types (such as `cond`, derived from `if`,\n",
- " or `let`, derived from `lambda`), and dotted list notation.\n",
+ " expression types (such as `cond` and `let`), and dotted list notation.\n",
" - **Semantics**: Missing `call/cc` and tail recursion. \n",
" - **Data Types**: Missing strings, characters, booleans, ports,\n",
" vectors, exact/inexact numbers. A Scheme list should actually be a custom data class, not a Python list.\n",
" - **Procedures**: Missing over 100 primitive procedures.\n",
" - **Error recovery**: Lispy does not attempt to detect,\n",
" reasonably report, or recover from errors. Lispy expects the\n",
- " programmer to be perfect. (That is an environment issue, not a language issue.)\n",
- "- **Good**: That's up to the readers to decide. I think that Lispy is good for my purpose of explaining Lisp interpreters.\n",
- "- **Fast**: Lispy computes (fact 100) in about a millisecond. That's fast enough for me (although slower than some\n",
- "other ways of computing it). "
+ " programmer to be perfect. \n",
+ "- **Good**: That's up to the readers to decide. I think that Lispy is good for my purpose of explaining Lisp interpreters. It is not a viable choice for serious software development.\n",
+ "- **Fast**: Lispy computes (factorial 100) in less than a millisecond. That's fast enough for me.
"
]
},
{
"cell_type": "code",
- "execution_count": 13,
- "id": "ececc132-11f2-4f76-80d4-771b89f93f14",
+ "execution_count": 21,
+ "id": "5bcc6a7f-261b-4156-92d3-c771ecfcd109",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "CPU times: user 371 μs, sys: 108 μs, total: 479 μs\n",
- "Wall time: 484 μs\n"
+ "93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000\n",
+ "CPU times: user 395 μs, sys: 200 μs, total: 595 μs\n",
+ "Wall time: 521 μs\n"
]
- },
- {
- "data": {
- "text/plain": [
- "93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000"
- ]
- },
- "execution_count": 13,
- "metadata": {},
- "output_type": "execute_result"
}
],
"source": [
- "fact = \"\"\"(begin (define fact (lambda (n) \n",
- " (if (<= n 1) \n",
- " 1 \n",
- " (* n (fact (- n 1)))))) \n",
- " (fact 100))\"\"\"\n",
- "\n",
- "%time eval(parse(fact))"
+ "%%time\n",
+ "batch(\"\"\"\n",
+ "(begin \n",
+ " (define (factorial n) \n",
+ " (if (<= n 1) \n",
+ " 1 \n",
+ " (* n (factorial (- n 1)))))\n",
+ " (factorial 100))\"\"\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "802789ab-2f1c-4fb0-a79b-c7621a851e81",
+ "metadata": {},
+ "source": [
+ "## More Example Lisp Programs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "f8728be6-2423-487f-a44a-49a50411d363",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(count 3 4)\n"
+ ]
+ }
+ ],
+ "source": [
+ "batch(\"\"\"\n",
+ "(list \n",
+ " (define (count item L) \n",
+ " (if (null? L)\n",
+ " 0\n",
+ " (+ (equal? item (first L)) (count item (rest L)))))\n",
+ " (count 0 (list 0 1 2 3 0 0))\n",
+ " (count (quote the) (quote (the more the merrier the bigger the better))))\"\"\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "af58a444-c684-481c-a74b-1ad6bb49459b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(square compose repeat 10 81 65536 2.0)\n"
+ ]
+ }
+ ],
+ "source": [
+ "batch(\"\"\"\n",
+ "(list\n",
+ " (define (square x) (* x x))\n",
+ " (define (compose f g) (lambda (x) (f (g x))))\n",
+ " (define (repeat f) (compose f f))\n",
+ " ((compose round sqrt) 101)\n",
+ " ((repeat square) 3)\n",
+ " ((repeat (repeat square)) 2)\n",
+ " ((repeat (repeat sqrt)) (pow 2 16)))\"\"\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "5dba7073-33d3-4f39-a192-5e0ccdc42f8a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765)\n"
+ ]
+ }
+ ],
+ "source": [
+ "batch(\"\"\"\n",
+ "(begin\n",
+ " (define (fib n) \n",
+ " (if (< n 2) \n",
+ " 1 \n",
+ " (+ (fib (- n 1)) (fib (- n 2)))))\n",
+ " (define (range start stop)\n",
+ " (if (= start stop) \n",
+ " (quote ()) \n",
+ " (cons start (range (+ start 1) stop))))\n",
+ " (map fib (range 0 20)))\"\"\")"
]
},
{
@@ -666,7 +860,7 @@
"source": [
"## True Story\n",
"\n",
- "To back up the idea that it can be very helpful to know how\n",
+ "To back up the idea that it can be helpful to know how\n",
"interpreters work, here's a story. Way back in 1984 I was writing a\n",
"Ph.D. thesis. This was before LaTeX, before Microsoft Word for Windows–we used\n",
"[troff](https://en.wikipedia.org/wiki/Troff). Unfortunately, troff had no facility for forward references\n",
@@ -681,7 +875,7 @@
"From there Tony and I split paths. He reasoned that the hard part was\n",
"the interpreter for expressions; he needed Lisp for that, but he knew\n",
"how to write a tiny C routine\n",
- "for reading the characters one at a time, and how to link the C routine into the Lisp\n",
+ "for reading the characters one at a time, and how to link it into the Lisp\n",
"program. I didn't know how to do that linking, but I reasoned that writing an\n",
"interpreter for this trivial language (all it had was set variable,\n",
"fetch variable, and string concatenate) was easy, so I wrote an\n",
@@ -692,9 +886,9 @@
"\n",
"
Further Reading
\n",
"\n",
+ "Years ago, I showed how to write a semi-practical near-complete Scheme interpreter (one in [Java](https://norvig.com/jscheme.html) and one in [Common Lisp](https://github.com/norvig/paip-lisp/blob/main/docs/chapter22.md)). I also have another page describing a more advanced version of Lispy.\n",
" \n",
- " \n",
- " To learn more about Scheme consult some of the fine books (by\n",
+ "To learn more about Scheme consult some of the fine books (by\n",
" Friedman\n",
" and Fellesein,\n",
@@ -716,8 +910,7 @@
" reference\n",
" manual.\n",
- "\n",
- "
I also have another page describing a more advanced version of Lispy."
+ "\n"
]
}
],