sync with O'Reilly Atlas
This commit is contained in:
parent
f0f160844d
commit
23e78eeb82
21
02-array-seq/lispy/LICENSE
Normal file
21
02-array-seq/lispy/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2010-2017 Peter Norvig
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
36
02-array-seq/lispy/README.md
Normal file
36
02-array-seq/lispy/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Norvig's originals and updates
|
||||
|
||||
This directory contains:
|
||||
|
||||
* `original/`:
|
||||
Norvig's [`lis.py`](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lis.py),
|
||||
[`lispy.py`](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lispy.py), and the `lispytest.py` custom test script for testing both;
|
||||
* `py3.10/`: `lis.py` with type hints, pattern matching, and minor edits—requires Python 3.10.
|
||||
|
||||
The `py3.10/` directory also has `lis_test.py` to run with
|
||||
[pytest](https://docs.pytest.org), including all the
|
||||
[`lis_tests` suite](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/py/lispytest.py#L5)
|
||||
from `original/lispytest.py`,
|
||||
and additional separate tests for each expression and special form handled by `evaluate`.
|
||||
|
||||
|
||||
## Provenance, Copyright and License
|
||||
|
||||
`lis.py` is
|
||||
[published](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lis.py)
|
||||
in the [norvig/pytudes](https://github.com/norvig/pytudes) repository on Github.
|
||||
The copyright holder is Peter Norvig and the code is licensed under the
|
||||
[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE).
|
||||
|
||||
|
||||
## Changes to Norvig's code
|
||||
|
||||
I made small changes to the programs in `original/`:
|
||||
|
||||
* In `lis.py`:
|
||||
* The `Procedure` class accepts a list of expressions as the `body`, and `__call__` evaluates all those expressions in order, returning the value of the last. This is consistent with Scheme's `lambda` syntax and provided a useful example for pattern matching.
|
||||
* In the `elif` block for `'lambda'`, I added the `*` in front of the `*body` variable in the tuple unpacking to capture the expressions as a list, before calling the `Procedure` constructor.
|
||||
|
||||
* In `lispy.py` I made [changes and a pull request](https://github.com/norvig/pytudes/pull/106) to make it run on Python 3.
|
||||
|
||||
_Luciano Ramalho<br/>June 29, 2021_
|
21
02-array-seq/lispy/original/LICENSE
Normal file
21
02-array-seq/lispy/original/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2010-2017 Peter Norvig
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
8
02-array-seq/lispy/original/README.md
Normal file
8
02-array-seq/lispy/original/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
# Source of the originals
|
||||
|
||||
* [lis.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py)
|
||||
|
||||
* [lispy.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py)
|
||||
|
||||
* [lispytest.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py)
|
132
02-array-seq/lispy/original/lis.py
Normal file
132
02-array-seq/lispy/original/lis.py
Normal file
@ -0,0 +1,132 @@
|
||||
################ Lispy: Scheme Interpreter in Python 3.3+
|
||||
|
||||
## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html
|
||||
|
||||
################ Imports and Types
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from collections import ChainMap as Environment
|
||||
|
||||
Symbol = str # A Lisp Symbol is implemented as a Python str
|
||||
List = list # A Lisp List is implemented as a Python list
|
||||
Number = (int, float) # A Lisp Number is implemented as a Python int or float
|
||||
|
||||
class Procedure(object):
|
||||
"A user-defined Scheme procedure."
|
||||
def __init__(self, parms, body, env):
|
||||
self.parms, self.body, self.env = parms, body, env
|
||||
def __call__(self, *args):
|
||||
env = Environment(dict(zip(self.parms, args)), self.env)
|
||||
return eval(self.body, env)
|
||||
|
||||
################ Global Environment
|
||||
|
||||
def standard_env():
|
||||
"An environment with some Scheme standard procedures."
|
||||
env = {}
|
||||
env.update(vars(math)) # sin, cos, sqrt, pi, ...
|
||||
env.update({
|
||||
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv,
|
||||
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
|
||||
'abs': abs,
|
||||
'append': op.add,
|
||||
'apply': lambda proc, args: proc(*args),
|
||||
'begin': lambda *x: x[-1],
|
||||
'car': lambda x: x[0],
|
||||
'cdr': lambda x: x[1:],
|
||||
'cons': lambda x,y: [x] + y,
|
||||
'eq?': op.is_,
|
||||
'equal?': op.eq,
|
||||
'length': len,
|
||||
'list': lambda *x: list(x),
|
||||
'list?': lambda x: isinstance(x,list),
|
||||
'map': lambda *args: list(map(*args)),
|
||||
'max': max,
|
||||
'min': min,
|
||||
'not': op.not_,
|
||||
'null?': lambda x: x == [],
|
||||
'number?': lambda x: isinstance(x, Number),
|
||||
'procedure?': callable,
|
||||
'round': round,
|
||||
'symbol?': lambda x: isinstance(x, Symbol),
|
||||
})
|
||||
return env
|
||||
|
||||
global_env = standard_env()
|
||||
|
||||
################ Parsing: parse, tokenize, and read_from_tokens
|
||||
|
||||
def parse(program):
|
||||
"Read a Scheme expression from a string."
|
||||
return read_from_tokens(tokenize(program))
|
||||
|
||||
def tokenize(s):
|
||||
"Convert a string into a list of tokens."
|
||||
return s.replace('(',' ( ').replace(')',' ) ').split()
|
||||
|
||||
def read_from_tokens(tokens):
|
||||
"Read an expression from a sequence of tokens."
|
||||
if len(tokens) == 0:
|
||||
raise SyntaxError('unexpected EOF while reading')
|
||||
token = tokens.pop(0)
|
||||
if '(' == token:
|
||||
L = []
|
||||
while tokens[0] != ')':
|
||||
L.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # pop off ')'
|
||||
return L
|
||||
elif ')' == token:
|
||||
raise SyntaxError('unexpected )')
|
||||
else:
|
||||
return atom(token)
|
||||
|
||||
def atom(token):
|
||||
"Numbers become numbers; every other token is a symbol."
|
||||
try: return int(token)
|
||||
except ValueError:
|
||||
try: return float(token)
|
||||
except ValueError:
|
||||
return Symbol(token)
|
||||
|
||||
################ Interaction: A REPL
|
||||
|
||||
def repl(prompt='lis.py> '):
|
||||
"A prompt-read-eval-print loop."
|
||||
while True:
|
||||
val = eval(parse(input(prompt)))
|
||||
if val is not None:
|
||||
print(lispstr(val))
|
||||
|
||||
def lispstr(exp):
|
||||
"Convert a Python object back into a Lisp-readable string."
|
||||
if isinstance(exp, List):
|
||||
return '(' + ' '.join(map(lispstr, exp)) + ')'
|
||||
else:
|
||||
return str(exp)
|
||||
|
||||
################ eval
|
||||
|
||||
def eval(x, env=global_env):
|
||||
"Evaluate an expression in an environment."
|
||||
if isinstance(x, Symbol): # variable reference
|
||||
return env[x]
|
||||
elif not isinstance(x, List): # constant literal
|
||||
return x
|
||||
elif x[0] == 'quote': # (quote exp)
|
||||
(_, exp) = x
|
||||
return exp
|
||||
elif x[0] == 'if': # (if test conseq alt)
|
||||
(_, test, conseq, alt) = x
|
||||
exp = (conseq if eval(test, env) else alt)
|
||||
return eval(exp, env)
|
||||
elif x[0] == 'define': # (define var exp)
|
||||
(_, var, exp) = x
|
||||
env[var] = eval(exp, env)
|
||||
elif x[0] == 'lambda': # (lambda (var...) body)
|
||||
(_, parms, body) = x
|
||||
return Procedure(parms, body, env)
|
||||
else: # (proc arg...)
|
||||
proc = eval(x[0], env)
|
||||
args = [eval(exp, env) for exp in x[1:]]
|
||||
return proc(*args)
|
316
02-array-seq/lispy/original/lispy.py
Normal file
316
02-array-seq/lispy/original/lispy.py
Normal file
@ -0,0 +1,316 @@
|
||||
################ Scheme Interpreter in Python
|
||||
|
||||
## (c) Peter Norvig, 2010; See http://norvig.com/lispy2.html
|
||||
|
||||
################ Symbol, Procedure, classes
|
||||
|
||||
import re, sys, io
|
||||
|
||||
class Symbol(str): pass
|
||||
|
||||
def Sym(s, symbol_table={}):
|
||||
"Find or create unique Symbol entry for str s in symbol table."
|
||||
if s not in symbol_table: symbol_table[s] = Symbol(s)
|
||||
return symbol_table[s]
|
||||
|
||||
_quote, _if, _set, _define, _lambda, _begin, _definemacro, = map(Sym,
|
||||
"quote if set! define lambda begin define-macro".split())
|
||||
|
||||
_quasiquote, _unquote, _unquotesplicing = map(Sym,
|
||||
"quasiquote unquote unquote-splicing".split())
|
||||
|
||||
class Procedure:
|
||||
"A user-defined Scheme procedure."
|
||||
def __init__(self, parms, exp, env):
|
||||
self.parms, self.exp, self.env = parms, exp, env
|
||||
def __call__(self, *args):
|
||||
return eval(self.exp, Env(self.parms, args, self.env))
|
||||
|
||||
################ parse, read, and user interaction
|
||||
|
||||
def parse(inport):
|
||||
"Parse a program: read and expand/error-check it."
|
||||
# Backwards compatibility: given a str, convert it to an InPort
|
||||
if isinstance(inport, str): inport = InPort(io.StringIO(inport))
|
||||
return expand(read(inport), toplevel=True)
|
||||
|
||||
eof_object = Symbol('#<eof-object>') # Note: uninterned; can't be read
|
||||
|
||||
class InPort:
|
||||
"An input port. Retains a line of chars."
|
||||
tokenizer = r"""\s*(,@|[('`,)]|"(?:[\\].|[^\\"])*"|;.*|[^\s('"`,;)]*)(.*)"""
|
||||
def __init__(self, file):
|
||||
self.file = file; self.line = ''
|
||||
def next_token(self):
|
||||
"Return the next token, reading new text into line buffer if needed."
|
||||
while True:
|
||||
if self.line == '': self.line = self.file.readline()
|
||||
if self.line == '': return eof_object
|
||||
token, self.line = re.match(InPort.tokenizer, self.line).groups()
|
||||
if token != '' and not token.startswith(';'):
|
||||
return token
|
||||
|
||||
def readchar(inport):
|
||||
"Read the next character from an input port."
|
||||
if inport.line != '':
|
||||
ch, inport.line = inport.line[0], inport.line[1:]
|
||||
return ch
|
||||
else:
|
||||
return inport.file.read(1) or eof_object
|
||||
|
||||
def read(inport):
|
||||
"Read a Scheme expression from an input port."
|
||||
def read_ahead(token):
|
||||
if '(' == token:
|
||||
L = []
|
||||
while True:
|
||||
token = inport.next_token()
|
||||
if token == ')': return L
|
||||
else: L.append(read_ahead(token))
|
||||
elif ')' == token: raise SyntaxError('unexpected )')
|
||||
elif token in quotes: return [quotes[token], read(inport)]
|
||||
elif token is eof_object: raise SyntaxError('unexpected EOF in list')
|
||||
else: return atom(token)
|
||||
# body of read:
|
||||
token1 = inport.next_token()
|
||||
return eof_object if token1 is eof_object else read_ahead(token1)
|
||||
|
||||
quotes = {"'":_quote, "`":_quasiquote, ",":_unquote, ",@":_unquotesplicing}
|
||||
|
||||
def atom(token):
|
||||
'Numbers become numbers; #t and #f are booleans; "..." string; otherwise Symbol.'
|
||||
if token == '#t': return True
|
||||
elif token == '#f': return False
|
||||
elif token[0] == '"': return token[1:-1]
|
||||
try: return int(token)
|
||||
except ValueError:
|
||||
try: return float(token)
|
||||
except ValueError:
|
||||
try: return complex(token.replace('i', 'j', 1))
|
||||
except ValueError:
|
||||
return Sym(token)
|
||||
|
||||
def to_string(x):
|
||||
"Convert a Python object back into a Lisp-readable string."
|
||||
if x is True: return "#t"
|
||||
elif x is False: return "#f"
|
||||
elif isa(x, Symbol): return x
|
||||
elif isa(x, str): return repr(x)
|
||||
elif isa(x, list): return '('+' '.join(map(to_string, x))+')'
|
||||
elif isa(x, complex): return str(x).replace('j', 'i')
|
||||
else: return str(x)
|
||||
|
||||
def load(filename):
|
||||
"Eval every expression from a file."
|
||||
repl(None, InPort(open(filename)), None)
|
||||
|
||||
def repl(prompt='lispy> ', inport=InPort(sys.stdin), out=sys.stdout):
|
||||
"A prompt-read-eval-print loop."
|
||||
sys.stderr.write("Lispy version 2.0\n")
|
||||
while True:
|
||||
try:
|
||||
if prompt: sys.stderr.write(prompt)
|
||||
x = parse(inport)
|
||||
if x is eof_object: return
|
||||
val = eval(x)
|
||||
if val is not None and out: print(to_string(val), file=out)
|
||||
except Exception as e:
|
||||
print('%s: %s' % (type(e).__name__, e))
|
||||
|
||||
################ Environment class
|
||||
|
||||
class Env(dict):
|
||||
"An environment: a dict of {'var':val} pairs, with an outer Env."
|
||||
def __init__(self, parms=(), args=(), outer=None):
|
||||
# Bind parm list to corresponding args, or single parm to list of args
|
||||
self.outer = outer
|
||||
if isa(parms, Symbol):
|
||||
self.update({parms:list(args)})
|
||||
else:
|
||||
if len(args) != len(parms):
|
||||
raise TypeError('expected %s, given %s, '
|
||||
% (to_string(parms), to_string(args)))
|
||||
self.update(zip(parms,args))
|
||||
def find(self, var):
|
||||
"Find the innermost Env where var appears."
|
||||
if var in self: return self
|
||||
elif self.outer is None: raise LookupError(var)
|
||||
else: return self.outer.find(var)
|
||||
|
||||
def is_pair(x): return x != [] and isa(x, list)
|
||||
def cons(x, y): return [x]+y
|
||||
|
||||
def callcc(proc):
|
||||
"Call proc with current continuation; escape only"
|
||||
ball = RuntimeWarning("Sorry, can't continue this continuation any longer.")
|
||||
def throw(retval): ball.retval = retval; raise ball
|
||||
try:
|
||||
return proc(throw)
|
||||
except RuntimeWarning as w:
|
||||
if w is ball: return ball.retval
|
||||
else: raise w
|
||||
|
||||
def add_globals(self):
|
||||
"Add some Scheme standard procedures."
|
||||
import math, cmath, operator as op
|
||||
self.update(vars(math))
|
||||
self.update(vars(cmath))
|
||||
self.update({
|
||||
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, 'not':op.not_,
|
||||
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
|
||||
'equal?':op.eq, 'eq?':op.is_, 'length':len, 'cons':cons,
|
||||
'car':lambda x:x[0], 'cdr':lambda x:x[1:], 'append':op.add,
|
||||
'list':lambda *x:list(x), 'list?': lambda x:isa(x,list),
|
||||
'null?':lambda x:x==[], 'symbol?':lambda x: isa(x, Symbol),
|
||||
'boolean?':lambda x: isa(x, bool), 'pair?':is_pair,
|
||||
'port?': lambda x:isa(x,file), 'apply':lambda proc,l: proc(*l),
|
||||
'eval':lambda x: eval(expand(x)), 'load':lambda fn: load(fn), 'call/cc':callcc,
|
||||
'open-input-file':open,'close-input-port':lambda p: p.file.close(),
|
||||
'open-output-file':lambda f:open(f,'w'), 'close-output-port':lambda p: p.close(),
|
||||
'eof-object?':lambda x:x is eof_object, 'read-char':readchar,
|
||||
'read':read, 'write':lambda x,port=sys.stdout:port.write(to_string(x)),
|
||||
'display':lambda x,port=sys.stdout:port.write(x if isa(x,str) else to_string(x))})
|
||||
return self
|
||||
|
||||
isa = isinstance
|
||||
|
||||
global_env = add_globals(Env())
|
||||
|
||||
################ eval (tail recursive)
|
||||
|
||||
def eval(x, env=global_env):
|
||||
"Evaluate an expression in an environment."
|
||||
while True:
|
||||
if isa(x, Symbol): # variable reference
|
||||
return env.find(x)[x]
|
||||
elif not isa(x, list): # constant literal
|
||||
return x
|
||||
elif x[0] is _quote: # (quote exp)
|
||||
(_, exp) = x
|
||||
return exp
|
||||
elif x[0] is _if: # (if test conseq alt)
|
||||
(_, test, conseq, alt) = x
|
||||
x = (conseq if eval(test, env) else alt)
|
||||
elif x[0] is _set: # (set! var exp)
|
||||
(_, var, exp) = x
|
||||
env.find(var)[var] = eval(exp, env)
|
||||
return None
|
||||
elif x[0] is _define: # (define var exp)
|
||||
(_, var, exp) = x
|
||||
env[var] = eval(exp, env)
|
||||
return None
|
||||
elif x[0] is _lambda: # (lambda (var*) exp)
|
||||
(_, vars, exp) = x
|
||||
return Procedure(vars, exp, env)
|
||||
elif x[0] is _begin: # (begin exp+)
|
||||
for exp in x[1:-1]:
|
||||
eval(exp, env)
|
||||
x = x[-1]
|
||||
else: # (proc exp*)
|
||||
exps = [eval(exp, env) for exp in x]
|
||||
proc = exps.pop(0)
|
||||
if isa(proc, Procedure):
|
||||
x = proc.exp
|
||||
env = Env(proc.parms, exps, proc.env)
|
||||
else:
|
||||
return proc(*exps)
|
||||
|
||||
################ expand
|
||||
|
||||
def expand(x, toplevel=False):
|
||||
"Walk tree of x, making optimizations/fixes, and signaling SyntaxError."
|
||||
require(x, x!=[]) # () => Error
|
||||
if not isa(x, list): # constant => unchanged
|
||||
return x
|
||||
elif x[0] is _quote: # (quote exp)
|
||||
require(x, len(x)==2)
|
||||
return x
|
||||
elif x[0] is _if:
|
||||
if len(x)==3: x = x + [None] # (if t c) => (if t c None)
|
||||
require(x, len(x)==4)
|
||||
return list(map(expand, x))
|
||||
elif x[0] is _set:
|
||||
require(x, len(x)==3);
|
||||
var = x[1] # (set! non-var exp) => Error
|
||||
require(x, isa(var, Symbol), "can set! only a symbol")
|
||||
return [_set, var, expand(x[2])]
|
||||
elif x[0] is _define or x[0] is _definemacro:
|
||||
require(x, len(x)>=3)
|
||||
_def, v, body = x[0], x[1], x[2:]
|
||||
if isa(v, list) and v: # (define (f args) body)
|
||||
f, args = v[0], v[1:] # => (define f (lambda (args) body))
|
||||
return expand([_def, f, [_lambda, args]+body])
|
||||
else:
|
||||
require(x, len(x)==3) # (define non-var/list exp) => Error
|
||||
require(x, isa(v, Symbol), "can define only a symbol")
|
||||
exp = expand(x[2])
|
||||
if _def is _definemacro:
|
||||
require(x, toplevel, "define-macro only allowed at top level")
|
||||
proc = eval(exp)
|
||||
require(x, callable(proc), "macro must be a procedure")
|
||||
macro_table[v] = proc # (define-macro v proc)
|
||||
return None # => None; add v:proc to macro_table
|
||||
return [_define, v, exp]
|
||||
elif x[0] is _begin:
|
||||
if len(x)==1: return None # (begin) => None
|
||||
else: return [expand(xi, toplevel) for xi in x]
|
||||
elif x[0] is _lambda: # (lambda (x) e1 e2)
|
||||
require(x, len(x)>=3) # => (lambda (x) (begin e1 e2))
|
||||
vars, body = x[1], x[2:]
|
||||
require(x, (isa(vars, list) and all(isa(v, Symbol) for v in vars))
|
||||
or isa(vars, Symbol), "illegal lambda argument list")
|
||||
exp = body[0] if len(body) == 1 else [_begin] + body
|
||||
return [_lambda, vars, expand(exp)]
|
||||
elif x[0] is _quasiquote: # `x => expand_quasiquote(x)
|
||||
require(x, len(x)==2)
|
||||
return expand_quasiquote(x[1])
|
||||
elif isa(x[0], Symbol) and x[0] in macro_table:
|
||||
return expand(macro_table[x[0]](*x[1:]), toplevel) # (m arg...)
|
||||
else: # => macroexpand if m isa macro
|
||||
return list(map(expand, x)) # (f arg...) => expand each
|
||||
|
||||
def require(x, predicate, msg="wrong length"):
|
||||
"Signal a syntax error if predicate is false."
|
||||
if not predicate: raise SyntaxError(to_string(x)+': '+msg)
|
||||
|
||||
_append, _cons, _let = map(Sym, "append cons let".split())
|
||||
|
||||
def expand_quasiquote(x):
|
||||
"""Expand `x => 'x; `,x => x; `(,@x y) => (append x y) """
|
||||
if not is_pair(x):
|
||||
return [_quote, x]
|
||||
require(x, x[0] is not _unquotesplicing, "can't splice here")
|
||||
if x[0] is _unquote:
|
||||
require(x, len(x)==2)
|
||||
return x[1]
|
||||
elif is_pair(x[0]) and x[0][0] is _unquotesplicing:
|
||||
require(x[0], len(x[0])==2)
|
||||
return [_append, x[0][1], expand_quasiquote(x[1:])]
|
||||
else:
|
||||
return [_cons, expand_quasiquote(x[0]), expand_quasiquote(x[1:])]
|
||||
|
||||
def let(*args):
|
||||
args = list(args)
|
||||
x = cons(_let, args)
|
||||
require(x, len(args)>1)
|
||||
bindings, body = args[0], args[1:]
|
||||
require(x, all(isa(b, list) and len(b)==2 and isa(b[0], Symbol)
|
||||
for b in bindings), "illegal binding list")
|
||||
vars, vals = zip(*bindings)
|
||||
return [[_lambda, list(vars)]+list(map(expand, body))] + list(map(expand, vals))
|
||||
|
||||
macro_table = {_let:let} ## More macros can go here
|
||||
|
||||
eval(parse("""(begin
|
||||
|
||||
(define-macro and (lambda args
|
||||
(if (null? args) #t
|
||||
(if (= (length args) 1) (car args)
|
||||
`(if ,(car args) (and ,@(cdr args)) #f)))))
|
||||
|
||||
;; More macros can also go here
|
||||
|
||||
)"""))
|
||||
|
||||
if __name__ == '__main__':
|
||||
repl()
|
122
02-array-seq/lispy/original/lispytest.py
Normal file
122
02-array-seq/lispy/original/lispytest.py
Normal file
@ -0,0 +1,122 @@
|
||||
from __future__ import print_function
|
||||
|
||||
################ Tests for lis.py and lispy.py
|
||||
|
||||
lis_tests = [
|
||||
("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
|
||||
("(+ 2 2)", 4),
|
||||
("(+ (* 2 100) (* 1 10))", 210),
|
||||
("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
|
||||
("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
|
||||
("(define x 3)", None), ("x", 3), ("(+ x x)", 6),
|
||||
("((lambda (x) (+ x x)) 5)", 10),
|
||||
("(define twice (lambda (x) (* 2 x)))", None), ("(twice 5)", 10),
|
||||
("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
|
||||
("((compose list twice) 5)", [10]),
|
||||
("(define repeat (lambda (f) (compose f f)))", None),
|
||||
("((repeat twice) 5)", 20), ("((repeat (repeat twice)) 5)", 80),
|
||||
("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
|
||||
("(fact 3)", 6),
|
||||
("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000),
|
||||
("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
|
||||
("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
|
||||
("""(define combine (lambda (f)
|
||||
(lambda (x y)
|
||||
(if (null? x) (quote ())
|
||||
(f (list (car x) (car y))
|
||||
((combine f) (cdr x) (cdr y)))))))""", None),
|
||||
("(define zip (combine cons))", None),
|
||||
("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
|
||||
("""(define riff-shuffle (lambda (deck) (begin
|
||||
(define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
|
||||
(define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
|
||||
(define mid (lambda (seq) (/ (length seq) 2)))
|
||||
((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None),
|
||||
("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
|
||||
("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]),
|
||||
("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]),
|
||||
]
|
||||
|
||||
lispy_tests = [
|
||||
("()", SyntaxError), ("(set! x)", SyntaxError),
|
||||
("(define 3 4)", SyntaxError),
|
||||
("(quote 1 2)", SyntaxError), ("(if 1 2 3 4)", SyntaxError),
|
||||
("(lambda 3 3)", SyntaxError), ("(lambda (x))", SyntaxError),
|
||||
("""(if (= 1 2) (define-macro a 'a)
|
||||
(define-macro a 'b))""", SyntaxError),
|
||||
("(define (twice x) (* 2 x))", None), ("(twice 2)", 4),
|
||||
("(twice 2 2)", TypeError),
|
||||
("(define lyst (lambda items items))", None),
|
||||
("(lyst 1 2 3 (+ 2 2))", [1,2,3,4]),
|
||||
("(if 1 2)", 2),
|
||||
("(if (= 3 4) 2)", None),
|
||||
("(begin (define x 1) (set! x (+ x 1)) (+ x 1))", 3),
|
||||
("(define ((account bal) amt) (set! bal (+ bal amt)) bal)", None),
|
||||
("(define a1 (account 100))", None),
|
||||
("(a1 0)", 100), ("(a1 10)", 110), ("(a1 10)", 120),
|
||||
("""(define (newton guess function derivative epsilon)
|
||||
(define guess2 (- guess (/ (function guess) (derivative guess))))
|
||||
(if (< (abs (- guess guess2)) epsilon) guess2
|
||||
(newton guess2 function derivative epsilon)))""", None),
|
||||
("""(define (square-root a)
|
||||
(newton 1 (lambda (x) (- (* x x) a)) (lambda (x) (* 2 x)) 1e-8))""", None),
|
||||
("(> (square-root 200.) 14.14213)", True),
|
||||
("(< (square-root 200.) 14.14215)", True),
|
||||
("(= (square-root 200.) (sqrt 200.))", True),
|
||||
("""(define (sum-squares-range start end)
|
||||
(define (sumsq-acc start end acc)
|
||||
(if (> start end) acc (sumsq-acc (+ start 1) end (+ (* start start) acc))))
|
||||
(sumsq-acc start end 0))""", None),
|
||||
("(sum-squares-range 1 3000)", 9004500500), ## Tests tail recursion
|
||||
("(call/cc (lambda (throw) (+ 5 (* 10 (throw 1))))) ;; throw", 1),
|
||||
("(call/cc (lambda (throw) (+ 5 (* 10 1)))) ;; do not throw", 15),
|
||||
("""(call/cc (lambda (throw)
|
||||
(+ 5 (* 10 (call/cc (lambda (escape) (* 100 (escape 3)))))))) ; 1 level""", 35),
|
||||
("""(call/cc (lambda (throw)
|
||||
(+ 5 (* 10 (call/cc (lambda (escape) (* 100 (throw 3)))))))) ; 2 levels""", 3),
|
||||
("""(call/cc (lambda (throw)
|
||||
(+ 5 (* 10 (call/cc (lambda (escape) (* 100 1))))))) ; 0 levels""", 1005),
|
||||
("(* 1i 1i)", -1), ("(sqrt -1)", 1j),
|
||||
("(let ((a 1) (b 2)) (+ a b))", 3),
|
||||
("(let ((a 1) (b 2 3)) (+ a b))", SyntaxError),
|
||||
("(and 1 2 3)", 3), ("(and (> 2 1) 2 3)", 3), ("(and)", True),
|
||||
("(and (> 2 1) (> 2 3))", False),
|
||||
("(define-macro unless (lambda args `(if (not ,(car args)) (begin ,@(cdr args))))) ; test `", None),
|
||||
("(unless (= 2 (+ 1 1)) (display 2) 3 4)", None),
|
||||
(r'(unless (= 4 (+ 1 1)) (display 2) (display "\n") 3 4)', 4),
|
||||
("(quote x)", 'x'),
|
||||
("(quote (1 2 three))", [1, 2, 'three']),
|
||||
("'x", 'x'),
|
||||
("'(one 2 3)", ['one', 2, 3]),
|
||||
("(define L (list 1 2 3))", None),
|
||||
("`(testing ,@L testing)", ['testing',1,2,3,'testing']),
|
||||
("`(testing ,L testing)", ['testing',[1,2,3],'testing']),
|
||||
("`,@L", SyntaxError),
|
||||
("""'(1 ;test comments '
|
||||
;skip this line
|
||||
2 ; more ; comments ; ) )
|
||||
3) ; final comment""", [1,2,3]),
|
||||
]
|
||||
|
||||
def test(tests, name=''):
|
||||
"For each (exp, expected) test case, see if eval(parse(exp)) == expected."
|
||||
fails = 0
|
||||
for (x, expected) in tests:
|
||||
try:
|
||||
result = eval(parse(x))
|
||||
print(x, '=>', lispstr(result))
|
||||
ok = (result == expected)
|
||||
except Exception as e:
|
||||
print(x, '=raises=>', type(e).__name__, e)
|
||||
ok = isinstance(expected, type) and issubclass(expected, Exception) and isinstance(e, expected)
|
||||
if not ok:
|
||||
fails += 1
|
||||
print('FAIL!!! Expected', expected)
|
||||
print('%s %s: %d out of %d tests fail.' % ('*'*45, name, fails, len(tests)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
from lis import *
|
||||
test(lis_tests, 'lis.py')
|
||||
from lispy import *
|
||||
test(lis_tests+lispy_tests, 'lispy.py')
|
||||
|
188
02-array-seq/lispy/py3.10/lis.py
Normal file
188
02-array-seq/lispy/py3.10/lis.py
Normal file
@ -0,0 +1,188 @@
|
||||
################ Lispy: Scheme Interpreter in Python 3.10
|
||||
|
||||
## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html
|
||||
## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021)
|
||||
## by Luciano Ramalho, adding type hints and pattern matching.
|
||||
|
||||
################ Imports and Types
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from collections import ChainMap
|
||||
from collections.abc import MutableMapping, Iterator
|
||||
from itertools import chain
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
Symbol: TypeAlias = str
|
||||
Atom: TypeAlias = float | int | Symbol
|
||||
Expression: TypeAlias = Atom | list
|
||||
|
||||
Environment: TypeAlias = MutableMapping[Symbol, object]
|
||||
|
||||
|
||||
class Procedure:
|
||||
"A user-defined Scheme procedure."
|
||||
|
||||
def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment):
|
||||
self.parms = parms
|
||||
self.body = body
|
||||
self.env = env
|
||||
|
||||
def __call__(self, *args: Expression) -> Any:
|
||||
local_env = dict(zip(self.parms, args))
|
||||
env: Environment = ChainMap(local_env, self.env)
|
||||
for exp in self.body:
|
||||
result = evaluate(exp, env)
|
||||
return result
|
||||
|
||||
|
||||
################ Global Environment
|
||||
|
||||
def standard_env() -> Environment:
|
||||
"An environment with some Scheme standard procedures."
|
||||
env: Environment = {}
|
||||
env.update(vars(math)) # sin, cos, sqrt, pi, ...
|
||||
env.update({
|
||||
'+': op.add,
|
||||
'-': op.sub,
|
||||
'*': op.mul,
|
||||
'/': op.truediv,
|
||||
'//': op.floordiv,
|
||||
'>': op.gt,
|
||||
'<': op.lt,
|
||||
'>=': op.ge,
|
||||
'<=': op.le,
|
||||
'=': op.eq,
|
||||
'abs': abs,
|
||||
'append': lambda *args: list(chain(*args)),
|
||||
'apply': lambda proc, args: proc(*args),
|
||||
'begin': lambda *x: x[-1],
|
||||
'car': lambda x: x[0],
|
||||
'cdr': lambda x: x[1:],
|
||||
'cons': lambda x, y: [x] + y,
|
||||
'eq?': op.is_,
|
||||
'equal?': op.eq,
|
||||
'filter': lambda *args: list(filter(*args)),
|
||||
'length': len,
|
||||
'list': lambda *x: list(x),
|
||||
'list?': lambda x: isinstance(x, list),
|
||||
'map': lambda *args: list(map(*args)),
|
||||
'max': max,
|
||||
'min': min,
|
||||
'not': op.not_,
|
||||
'null?': lambda x: x == [],
|
||||
'number?': lambda x: isinstance(x, (int, float)),
|
||||
'procedure?': callable,
|
||||
'round': round,
|
||||
'symbol?': lambda x: isinstance(x, Symbol),
|
||||
})
|
||||
return env
|
||||
|
||||
################ Parsing: parse, tokenize, and read_from_tokens
|
||||
|
||||
def parse(program: str) -> Expression:
|
||||
"Read a Scheme expression from a string."
|
||||
return read_from_tokens(tokenize(program))
|
||||
|
||||
|
||||
def tokenize(s: str) -> list[str]:
|
||||
"Convert a string into a list of tokens."
|
||||
return s.replace('(', ' ( ').replace(')', ' ) ').split()
|
||||
|
||||
|
||||
def read_from_tokens(tokens: list[str]) -> Expression:
|
||||
"Read an expression from a sequence of tokens."
|
||||
if len(tokens) == 0:
|
||||
raise SyntaxError('unexpected EOF while reading')
|
||||
token = tokens.pop(0)
|
||||
if '(' == token:
|
||||
exp = []
|
||||
while tokens[0] != ')':
|
||||
exp.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # discard ')'
|
||||
return exp
|
||||
elif ')' == token:
|
||||
raise SyntaxError('unexpected )')
|
||||
else:
|
||||
return parse_atom(token)
|
||||
|
||||
|
||||
def parse_atom(token: str) -> Atom:
|
||||
"Numbers become numbers; every other token is a symbol."
|
||||
try:
|
||||
return int(token)
|
||||
except ValueError:
|
||||
try:
|
||||
return float(token)
|
||||
except ValueError:
|
||||
return Symbol(token)
|
||||
|
||||
|
||||
################ Interaction: A REPL
|
||||
|
||||
def repl(prompt: str = 'lis.py> ') -> None:
|
||||
"A prompt-read-evaluate-print loop."
|
||||
global_env = standard_env()
|
||||
while True:
|
||||
val = evaluate(parse(input(prompt)), global_env)
|
||||
if val is not None:
|
||||
print(lispstr(val))
|
||||
|
||||
|
||||
def lispstr(exp: object) -> str:
|
||||
"Convert a Python object back into a Lisp-readable string."
|
||||
if isinstance(exp, list):
|
||||
return '(' + ' '.join(map(lispstr, exp)) + ')'
|
||||
else:
|
||||
return str(exp)
|
||||
|
||||
|
||||
################ eval
|
||||
|
||||
# tag::EVALUATE[]
|
||||
def evaluate(exp: Expression, env: Environment) -> Any:
|
||||
"Evaluate an expression in an environment."
|
||||
match exp:
|
||||
case int(x) | float(x):
|
||||
return x
|
||||
case Symbol(var):
|
||||
return env[var]
|
||||
case []:
|
||||
return []
|
||||
case ['quote', exp]:
|
||||
return exp
|
||||
case ['if', test, consequence, alternative]:
|
||||
if evaluate(test, env):
|
||||
return evaluate(consequence, env)
|
||||
else:
|
||||
return evaluate(alternative, env)
|
||||
case ['define', Symbol(var), value_exp]:
|
||||
env[var] = evaluate(value_exp, env)
|
||||
case ['define', [Symbol(name), *parms], *body]:
|
||||
env[name] = Procedure(parms, body, env)
|
||||
case ['lambda', [*parms], *body]:
|
||||
return Procedure(parms, body, env)
|
||||
case [op, *args]:
|
||||
proc = evaluate(op, env)
|
||||
values = [evaluate(arg, env) for arg in args]
|
||||
return proc(*values)
|
||||
case _:
|
||||
raise SyntaxError(repr(exp))
|
||||
# end::EVALUATE[]
|
||||
|
||||
|
||||
################ non-interactive execution
|
||||
|
||||
|
||||
def run_lines(source: str) -> Iterator[Any]:
|
||||
global_env: Environment = standard_env()
|
||||
tokens = tokenize(source)
|
||||
while tokens:
|
||||
exp = read_from_tokens(tokens)
|
||||
yield evaluate(exp, global_env)
|
||||
|
||||
|
||||
def run(source: str) -> Any:
|
||||
for result in run_lines(source):
|
||||
pass
|
||||
return result
|
@ -12,7 +12,7 @@ from lis import parse, evaluate, Expression, Environment, standard_env
|
||||
('(sum 1 2 3)', ['sum', 1, 2, 3]),
|
||||
('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]),
|
||||
('99 100', 99), # parse stops at the first complete expression
|
||||
('(a)(b)', ['a']),
|
||||
('(a)(b)', ['a']),
|
||||
])
|
||||
def test_parse(source: str, expected: Expression) -> None:
|
||||
got = parse(source)
|
||||
@ -122,7 +122,7 @@ def test_lambda(std_env: Environment) -> None:
|
||||
source = '(lambda (a b) (if (>= a b) a b))'
|
||||
func = evaluate(parse(source), std_env)
|
||||
assert func.parms == ['a', 'b']
|
||||
assert func.body == ['if', ['>=', 'a', 'b'], 'a', 'b']
|
||||
assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
|
||||
assert func.env is std_env
|
||||
assert func(1, 2) == 2
|
||||
assert func(3, 2) == 3
|
||||
@ -176,7 +176,7 @@ def test_define_function(std_env: Environment) -> None:
|
||||
assert got is None
|
||||
max_fn = std_env['max']
|
||||
assert max_fn.parms == ['a', 'b']
|
||||
assert max_fn.body == ['if', ['>=', 'a', 'b'], 'a', 'b']
|
||||
assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
|
||||
assert max_fn.env is std_env
|
||||
assert max_fn(1, 2) == 2
|
||||
assert max_fn(3, 2) == 3
|
69
02-array-seq/lispy/py3.10/meta_test.py
Normal file
69
02-array-seq/lispy/py3.10/meta_test.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
Tests for developing a meta-circular interpreter, step-by-step.
|
||||
"""
|
||||
|
||||
|
||||
import operator as op
|
||||
|
||||
from lis import run
|
||||
|
||||
env_scm = """
|
||||
(define standard-env (list
|
||||
(list (quote +) +)
|
||||
(list (quote -) -)
|
||||
))
|
||||
standard-env
|
||||
"""
|
||||
|
||||
def test_env_build():
|
||||
got = run(env_scm)
|
||||
assert got == [['+', op.add], ['-', op.sub]]
|
||||
|
||||
scan_scm = """
|
||||
(define l (quote (a b c)))
|
||||
(define (scan what where)
|
||||
(if (null? where)
|
||||
()
|
||||
(if (eq? what (car where))
|
||||
what
|
||||
(scan what (cdr where))))
|
||||
)
|
||||
"""
|
||||
|
||||
def test_scan():
|
||||
source = scan_scm + '(scan (quote a) l )'
|
||||
got = run(source)
|
||||
assert got == 'a'
|
||||
|
||||
|
||||
def test_scan_not_found():
|
||||
source = scan_scm + '(scan (quote z) l )'
|
||||
got = run(source)
|
||||
assert got == []
|
||||
|
||||
|
||||
lookup_scm = """
|
||||
(define env (list
|
||||
(list (quote +) +)
|
||||
(list (quote -) -)
|
||||
))
|
||||
(define (lookup what where)
|
||||
(if (null? where)
|
||||
()
|
||||
(if (eq? what (car (car where)))
|
||||
(car (cdr (car where)))
|
||||
(lookup what (cdr where))))
|
||||
)
|
||||
"""
|
||||
|
||||
def test_lookup():
|
||||
source = lookup_scm + '(lookup (quote +) env)'
|
||||
got = run(source)
|
||||
assert got == op.add
|
||||
|
||||
|
||||
def test_lookup_not_found():
|
||||
source = lookup_scm + '(lookup (quote z) env )'
|
||||
got = run(source)
|
||||
assert got == []
|
||||
|
33
02-array-seq/lispy/py3.9-no-hints/README.md
Normal file
33
02-array-seq/lispy/py3.9-no-hints/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Changes from the original
|
||||
|
||||
While adapting Peter Norvig's [lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) for
|
||||
use in _Fluent Python, Second Edition_, I made a few changes for didactic reasons.
|
||||
|
||||
_Luciano Ramalho_
|
||||
|
||||
## Major changes
|
||||
|
||||
* Make the `lambda` form accept more than one expression as the body. This is consistent with _Scheme_ syntax, and provides a useful example for the book. To implement this:
|
||||
* In `Procedure.__call__`: evaluate `self.body` as a list of expressions, instead of a single expression. Return the value of the last expression.
|
||||
* In `evaluate()`: when processing `lambda`, unpack expression into `(_, parms, *body)`, to accept a list of expressions as the body.
|
||||
* Remove the `global_env` global `dict`. It is only used as a default value for the `env` parameter in `evaluate()`, but it is unsafe to use mutable data structures as parameter default values. To implement this:
|
||||
* In `repl()`: create local variable `global_env` and pass it as the `env` paramater of `evaluate()`.
|
||||
* In `evaluate()`, remove `global_env` default value for `env`.
|
||||
* Rewrite the custom test script
|
||||
[lispytest.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) as
|
||||
[lis_test.py](https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/lispy/py3.9/lis_test.py):
|
||||
a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's the test cases for
|
||||
[lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py)
|
||||
but removing the test cases for the features implemented only in
|
||||
[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py)
|
||||
|
||||
|
||||
## Minor changes
|
||||
|
||||
Cosmetic changes to make the code look more familiar to
|
||||
Python programmers, the audience of _Fluent Python_.
|
||||
|
||||
* Rename `eval()` to `evaluate()`, to avoid confusion with Python's `eval` built-in function.
|
||||
* Refer to the list class as `list` instead of aliasing as `List`, to avoid confusion with `typing.List` which is often imported as `List`.
|
||||
* Import `collections.ChainMap` as `ChainMap` instead of `Environment`.
|
||||
|
142
02-array-seq/lispy/py3.9-no-hints/lis.py
Normal file
142
02-array-seq/lispy/py3.9-no-hints/lis.py
Normal file
@ -0,0 +1,142 @@
|
||||
################ Lispy: Scheme Interpreter in Python 3.9
|
||||
|
||||
## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html
|
||||
## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021)
|
||||
## by Luciano Ramalho, adding type hints and pattern matching.
|
||||
|
||||
################ Imports and Types
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from collections import ChainMap
|
||||
from typing import Any
|
||||
|
||||
Symbol = str # A Lisp Symbol is implemented as a Python str
|
||||
Number = (int, float) # A Lisp Number is implemented as a Python int or float
|
||||
|
||||
class Procedure:
|
||||
"A user-defined Scheme procedure."
|
||||
def __init__(self, parms, body, env):
|
||||
self.parms, self.body, self.env = parms, body, env
|
||||
def __call__(self, *args):
|
||||
env = ChainMap(dict(zip(self.parms, args)), self.env)
|
||||
for exp in self.body:
|
||||
result = evaluate(exp, env)
|
||||
return result
|
||||
|
||||
|
||||
################ Global Environment
|
||||
|
||||
def standard_env():
|
||||
"An environment with some Scheme standard procedures."
|
||||
env = {}
|
||||
env.update(vars(math)) # sin, cos, sqrt, pi, ...
|
||||
env.update({
|
||||
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv,
|
||||
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
|
||||
'abs': abs,
|
||||
'append': op.add,
|
||||
'apply': lambda proc, args: proc(*args),
|
||||
'begin': lambda *x: x[-1],
|
||||
'car': lambda x: x[0],
|
||||
'cdr': lambda x: x[1:],
|
||||
'cons': lambda x,y: [x] + y,
|
||||
'eq?': op.is_,
|
||||
'equal?': op.eq,
|
||||
'length': len,
|
||||
'list': lambda *x: list(x),
|
||||
'list?': lambda x: isinstance(x,list),
|
||||
'map': lambda *args: list(map(*args)),
|
||||
'max': max,
|
||||
'min': min,
|
||||
'not': op.not_,
|
||||
'null?': lambda x: x == [],
|
||||
'number?': lambda x: isinstance(x, Number),
|
||||
'procedure?': callable,
|
||||
'round': round,
|
||||
'symbol?': lambda x: isinstance(x, Symbol),
|
||||
})
|
||||
return env
|
||||
|
||||
################ Parsing: parse, tokenize, and read_from_tokens
|
||||
|
||||
def parse(program):
|
||||
"Read a Scheme expression from a string."
|
||||
return read_from_tokens(tokenize(program))
|
||||
|
||||
def tokenize(s):
|
||||
"Convert a string into a list of tokens."
|
||||
return s.replace('(',' ( ').replace(')',' ) ').split()
|
||||
|
||||
def read_from_tokens(tokens):
|
||||
"Read an expression from a sequence of tokens."
|
||||
if len(tokens) == 0:
|
||||
raise SyntaxError('unexpected EOF while reading')
|
||||
token = tokens.pop(0)
|
||||
if '(' == token:
|
||||
exp = []
|
||||
while tokens[0] != ')':
|
||||
exp.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # discard ')'
|
||||
return exp
|
||||
elif ')' == token:
|
||||
raise SyntaxError('unexpected )')
|
||||
else:
|
||||
return parse_atom(token)
|
||||
|
||||
|
||||
def parse_atom(token: str):
|
||||
"Numbers become numbers; every other token is a symbol."
|
||||
try:
|
||||
return int(token)
|
||||
except ValueError:
|
||||
try:
|
||||
return float(token)
|
||||
except ValueError:
|
||||
return Symbol(token)
|
||||
|
||||
|
||||
################ Interaction: A REPL
|
||||
|
||||
def repl(prompt: str = 'lis.py> ') -> None:
|
||||
"A prompt-read-eval-print loop."
|
||||
global_env = standard_env()
|
||||
while True:
|
||||
val = evaluate(parse(input(prompt)), global_env)
|
||||
if val is not None:
|
||||
print(lispstr(val))
|
||||
|
||||
|
||||
def lispstr(exp):
|
||||
"Convert a Python object back into a Lisp-readable string."
|
||||
if isinstance(exp, list):
|
||||
return '(' + ' '.join(map(lispstr, exp)) + ')'
|
||||
else:
|
||||
return str(exp)
|
||||
|
||||
|
||||
################ eval
|
||||
|
||||
def evaluate(x, env):
|
||||
"Evaluate an expression in an environment."
|
||||
if isinstance(x, Symbol): # variable reference
|
||||
return env[x]
|
||||
elif not isinstance(x, list): # constant literal
|
||||
return x
|
||||
elif x[0] == 'quote': # (quote exp)
|
||||
(_, exp) = x
|
||||
return exp
|
||||
elif x[0] == 'if': # (if test conseq alt)
|
||||
(_, test, conseq, alt) = x
|
||||
exp = (conseq if evaluate(test, env) else alt)
|
||||
return evaluate(exp, env)
|
||||
elif x[0] == 'define': # (define var exp)
|
||||
(_, var, exp) = x
|
||||
env[var] = evaluate(exp, env)
|
||||
elif x[0] == 'lambda': # (lambda (var...) body)
|
||||
(_, parms, *body) = x
|
||||
return Procedure(parms, body, env)
|
||||
else: # (proc arg...)
|
||||
proc = evaluate(x[0], env)
|
||||
args = [evaluate(exp, env) for exp in x[1:]]
|
||||
return proc(*args)
|
166
02-array-seq/lispy/py3.9-no-hints/lis_test.py
Normal file
166
02-array-seq/lispy/py3.9-no-hints/lis_test.py
Normal file
@ -0,0 +1,166 @@
|
||||
from pytest import mark, fixture
|
||||
|
||||
from lis import parse, evaluate, standard_env
|
||||
|
||||
############################################################# tests for parse
|
||||
|
||||
@mark.parametrize( 'source, expected', [
|
||||
('7', 7),
|
||||
('x', 'x'),
|
||||
('(sum 1 2 3)', ['sum', 1, 2, 3]),
|
||||
('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]),
|
||||
('99 100', 99), # parse stops at the first complete expression
|
||||
('(a)(b)', ['a']),
|
||||
])
|
||||
def test_parse(source: str, expected):
|
||||
got = parse(source)
|
||||
assert got == expected
|
||||
|
||||
|
||||
########################################################## tests for evaluate
|
||||
|
||||
# Norvig's tests are not isolated: they assume the
|
||||
# same environment from first to last test.
|
||||
global_env_for_first_test = standard_env()
|
||||
|
||||
@mark.parametrize( 'source, expected', [
|
||||
("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
|
||||
("(+ 2 2)", 4),
|
||||
("(+ (* 2 100) (* 1 10))", 210),
|
||||
("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
|
||||
("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
|
||||
("(define x 3)", None),
|
||||
("x", 3),
|
||||
("(+ x x)", 6),
|
||||
("((lambda (x) (+ x x)) 5)", 10),
|
||||
("(define twice (lambda (x) (* 2 x)))", None),
|
||||
("(twice 5)", 10),
|
||||
("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
|
||||
("((compose list twice) 5)", [10]),
|
||||
("(define repeat (lambda (f) (compose f f)))", None),
|
||||
("((repeat twice) 5)", 20),
|
||||
("((repeat (repeat twice)) 5)", 80),
|
||||
("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
|
||||
("(fact 3)", 6),
|
||||
("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000),
|
||||
("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
|
||||
("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
|
||||
("""(define combine (lambda (f)
|
||||
(lambda (x y)
|
||||
(if (null? x) (quote ())
|
||||
(f (list (car x) (car y))
|
||||
((combine f) (cdr x) (cdr y)))))))""", None),
|
||||
("(define zip (combine cons))", None),
|
||||
("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
|
||||
("""(define riff-shuffle (lambda (deck)
|
||||
(begin
|
||||
(define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
|
||||
(define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
|
||||
(define mid (lambda (seq) (/ (length seq) 2)))
|
||||
((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None),
|
||||
("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
|
||||
("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]),
|
||||
("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]),
|
||||
])
|
||||
def test_evaluate(source, expected):
|
||||
got = evaluate(parse(source), global_env_for_first_test)
|
||||
assert got == expected
|
||||
|
||||
|
||||
@fixture
|
||||
def std_env():
|
||||
return standard_env()
|
||||
|
||||
# tests for each of the cases in evaluate
|
||||
|
||||
def test_evaluate_variable():
|
||||
env = dict(x=10)
|
||||
source = 'x'
|
||||
expected = 10
|
||||
got = evaluate(parse(source), env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_literal(std_env):
|
||||
source = '3.3'
|
||||
expected = 3.3
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_quote(std_env):
|
||||
source = '(quote (1.1 is not 1))'
|
||||
expected = [1.1, 'is', 'not', 1]
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_if_true(std_env) -> None:
|
||||
source = '(if 1 10 no-such-thing)'
|
||||
expected = 10
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_if_false(std_env) -> None:
|
||||
source = '(if 0 no-such-thing 20)'
|
||||
expected = 20
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_define(std_env) -> None:
|
||||
source = '(define answer (* 6 7))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got is None
|
||||
assert std_env['answer'] == 42
|
||||
|
||||
|
||||
def test_lambda(std_env) -> None:
|
||||
source = '(lambda (a b) (if (>= a b) a b))'
|
||||
func = evaluate(parse(source), std_env)
|
||||
assert func.parms == ['a', 'b']
|
||||
assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
|
||||
assert func.env is std_env
|
||||
assert func(1, 2) == 2
|
||||
assert func(3, 2) == 3
|
||||
|
||||
|
||||
def test_begin(std_env) -> None:
|
||||
source = """
|
||||
(begin
|
||||
(define x (* 2 3))
|
||||
(* x 7)
|
||||
)
|
||||
"""
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 42
|
||||
|
||||
|
||||
def test_invocation_builtin_car(std_env) -> None:
|
||||
source = '(car (quote (11 22 33)))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 11
|
||||
|
||||
|
||||
def test_invocation_builtin_append(std_env) -> None:
|
||||
source = '(append (quote (a b)) (quote (c d)))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == ['a', 'b', 'c', 'd']
|
||||
|
||||
|
||||
def test_invocation_builtin_map(std_env) -> None:
|
||||
source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == [2, 4, 6]
|
||||
|
||||
|
||||
def test_invocation_user_procedure(std_env):
|
||||
source = """
|
||||
(begin
|
||||
(define max (lambda (a b) (if (>= a b) a b)))
|
||||
(max 22 11)
|
||||
)
|
||||
"""
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 22
|
33
02-array-seq/lispy/py3.9/README.md
Normal file
33
02-array-seq/lispy/py3.9/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Changes from the original
|
||||
|
||||
While adapting Peter Norvig's [lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) for
|
||||
use in _Fluent Python, Second Edition_, I made a few changes for didactic reasons.
|
||||
|
||||
_Luciano Ramalho_
|
||||
|
||||
## Major changes
|
||||
|
||||
* Make the `lambda` form accept more than one expression as the body. This is consistent with _Scheme_ syntax, and provides a useful example for the book. To implement this:
|
||||
* In `Procedure.__call__`: evaluate `self.body` as a list of expressions, instead of a single expression. Return the value of the last expression.
|
||||
* In `evaluate()`: when processing `lambda`, unpack expression into `(_, parms, *body)`, to accept a list of expressions as the body.
|
||||
* Remove the `global_env` global `dict`. It is only used as a default value for the `env` parameter in `evaluate()`, but it is unsafe to use mutable data structures as parameter default values. To implement this:
|
||||
* In `repl()`: create local variable `global_env` and pass it as the `env` paramater of `evaluate()`.
|
||||
* In `evaluate()`, remove `global_env` default value for `env`.
|
||||
* Rewrite the custom test script
|
||||
[lispytest.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) as
|
||||
[lis_test.py](https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/lispy/py3.9/lis_test.py):
|
||||
a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's the test cases for
|
||||
[lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py)
|
||||
but removing the test cases for the features implemented only in
|
||||
[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py)
|
||||
|
||||
|
||||
## Minor changes
|
||||
|
||||
Cosmetic changes to make the code look more familiar to
|
||||
Python programmers, the audience of _Fluent Python_.
|
||||
|
||||
* Rename `eval()` to `evaluate()`, to avoid confusion with Python's `eval` built-in function.
|
||||
* Refer to the list class as `list` instead of aliasing as `List`, to avoid confusion with `typing.List` which is often imported as `List`.
|
||||
* Import `collections.ChainMap` as `ChainMap` instead of `Environment`.
|
||||
|
155
02-array-seq/lispy/py3.9/lis.py
Normal file
155
02-array-seq/lispy/py3.9/lis.py
Normal file
@ -0,0 +1,155 @@
|
||||
################ Lispy: Scheme Interpreter in Python 3.9
|
||||
|
||||
## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html
|
||||
## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021)
|
||||
## by Luciano Ramalho, adding type hints and pattern matching.
|
||||
|
||||
################ Imports and Types
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from collections import ChainMap
|
||||
from collections.abc import MutableMapping, Iterator
|
||||
from itertools import chain
|
||||
from typing import Any, Union
|
||||
|
||||
Symbol = str
|
||||
Atom = Union[float, int, Symbol]
|
||||
Expression = Union[Atom, list]
|
||||
|
||||
Environment = MutableMapping[Symbol, object]
|
||||
|
||||
|
||||
class Procedure:
|
||||
"A user-defined Scheme procedure."
|
||||
|
||||
def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment):
|
||||
self.parms = parms
|
||||
self.body = body
|
||||
self.env = env
|
||||
|
||||
def __call__(self, *args: Expression) -> Any:
|
||||
local_env = dict(zip(self.parms, args))
|
||||
env: Environment = ChainMap(local_env, self.env)
|
||||
for exp in self.body:
|
||||
result = evaluate(exp, env)
|
||||
return result
|
||||
|
||||
|
||||
################ Global Environment
|
||||
|
||||
def standard_env() -> Environment:
|
||||
"An environment with some Scheme standard procedures."
|
||||
env: Environment = {}
|
||||
env.update(vars(math)) # sin, cos, sqrt, pi, ...
|
||||
env.update({
|
||||
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv,
|
||||
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
|
||||
'abs': abs,
|
||||
'append': op.add,
|
||||
'apply': lambda proc, args: proc(*args),
|
||||
'begin': lambda *x: x[-1],
|
||||
'car': lambda x: x[0],
|
||||
'cdr': lambda x: x[1:],
|
||||
'cons': lambda x,y: [x] + y,
|
||||
'eq?': op.is_,
|
||||
'equal?': op.eq,
|
||||
'length': len,
|
||||
'list': lambda *x: list(x),
|
||||
'list?': lambda x: isinstance(x,list),
|
||||
'map': lambda *args: list(map(*args)),
|
||||
'max': max,
|
||||
'min': min,
|
||||
'not': op.not_,
|
||||
'null?': lambda x: x == [],
|
||||
'number?': lambda x: isinstance(x, (int, float)),
|
||||
'procedure?': callable,
|
||||
'round': round,
|
||||
'symbol?': lambda x: isinstance(x, Symbol),
|
||||
})
|
||||
return env
|
||||
|
||||
################ Parsing: parse, tokenize, and read_from_tokens
|
||||
|
||||
def parse(program: str) -> Expression:
|
||||
"Read a Scheme expression from a string."
|
||||
return read_from_tokens(tokenize(program))
|
||||
|
||||
|
||||
def tokenize(s: str) -> list[str]:
|
||||
"Convert a string into a list of tokens."
|
||||
return s.replace('(', ' ( ').replace(')', ' ) ').split()
|
||||
|
||||
|
||||
def read_from_tokens(tokens: list[str]) -> Expression:
|
||||
"Read an expression from a sequence of tokens."
|
||||
if len(tokens) == 0:
|
||||
raise SyntaxError('unexpected EOF while reading')
|
||||
token = tokens.pop(0)
|
||||
if '(' == token:
|
||||
exp = []
|
||||
while tokens[0] != ')':
|
||||
exp.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # discard ')'
|
||||
return exp
|
||||
elif ')' == token:
|
||||
raise SyntaxError('unexpected )')
|
||||
else:
|
||||
return parse_atom(token)
|
||||
|
||||
|
||||
def parse_atom(token: str) -> Atom:
|
||||
"Numbers become numbers; every other token is a symbol."
|
||||
try:
|
||||
return int(token)
|
||||
except ValueError:
|
||||
try:
|
||||
return float(token)
|
||||
except ValueError:
|
||||
return Symbol(token)
|
||||
|
||||
|
||||
################ Interaction: A REPL
|
||||
|
||||
def repl(prompt: str = 'lis.py> ') -> None:
|
||||
"A prompt-read-eval-print loop."
|
||||
global_env = standard_env()
|
||||
while True:
|
||||
val = evaluate(parse(input(prompt)), global_env)
|
||||
if val is not None:
|
||||
print(lispstr(val))
|
||||
|
||||
|
||||
def lispstr(exp: object) -> str:
|
||||
"Convert a Python object back into a Lisp-readable string."
|
||||
if isinstance(exp, list):
|
||||
return '(' + ' '.join(map(lispstr, exp)) + ')'
|
||||
else:
|
||||
return str(exp)
|
||||
|
||||
|
||||
################ eval
|
||||
|
||||
def evaluate(x: Expression, env: Environment) -> Any:
|
||||
"Evaluate an expression in an environment."
|
||||
if isinstance(x, Symbol): # variable reference
|
||||
return env[x]
|
||||
elif not isinstance(x, list): # constant literal
|
||||
return x
|
||||
elif x[0] == 'quote': # (quote exp)
|
||||
(_, exp) = x
|
||||
return exp
|
||||
elif x[0] == 'if': # (if test conseq alt)
|
||||
(_, test, conseq, alt) = x
|
||||
exp = (conseq if evaluate(test, env) else alt)
|
||||
return evaluate(exp, env)
|
||||
elif x[0] == 'define': # (define var exp)
|
||||
(_, var, exp) = x
|
||||
env[var] = evaluate(exp, env)
|
||||
elif x[0] == 'lambda': # (lambda (var...) body)
|
||||
(_, parms, *body) = x
|
||||
return Procedure(parms, body, env)
|
||||
else: # (proc arg...)
|
||||
proc = evaluate(x[0], env)
|
||||
args = [evaluate(exp, env) for exp in x[1:]]
|
||||
return proc(*args)
|
168
02-array-seq/lispy/py3.9/lis_test.py
Normal file
168
02-array-seq/lispy/py3.9/lis_test.py
Normal file
@ -0,0 +1,168 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from pytest import mark, fixture
|
||||
|
||||
from lis import parse, evaluate, standard_env, Symbol, Environment, Expression
|
||||
|
||||
############################################################# tests for parse
|
||||
|
||||
@mark.parametrize( 'source, expected', [
|
||||
('7', 7),
|
||||
('x', 'x'),
|
||||
('(sum 1 2 3)', ['sum', 1, 2, 3]),
|
||||
('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]),
|
||||
('99 100', 99), # parse stops at the first complete expression
|
||||
('(a)(b)', ['a']),
|
||||
])
|
||||
def test_parse(source: str, expected: Expression) -> None:
|
||||
got = parse(source)
|
||||
assert got == expected
|
||||
|
||||
|
||||
########################################################## tests for evaluate
|
||||
|
||||
# Norvig's tests are not isolated: they assume the
|
||||
# same environment from first to last test.
|
||||
global_env_for_first_test = standard_env()
|
||||
|
||||
@mark.parametrize( 'source, expected', [
|
||||
("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
|
||||
("(+ 2 2)", 4),
|
||||
("(+ (* 2 100) (* 1 10))", 210),
|
||||
("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
|
||||
("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
|
||||
("(define x 3)", None),
|
||||
("x", 3),
|
||||
("(+ x x)", 6),
|
||||
("((lambda (x) (+ x x)) 5)", 10),
|
||||
("(define twice (lambda (x) (* 2 x)))", None),
|
||||
("(twice 5)", 10),
|
||||
("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
|
||||
("((compose list twice) 5)", [10]),
|
||||
("(define repeat (lambda (f) (compose f f)))", None),
|
||||
("((repeat twice) 5)", 20),
|
||||
("((repeat (repeat twice)) 5)", 80),
|
||||
("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
|
||||
("(fact 3)", 6),
|
||||
("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000),
|
||||
("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
|
||||
("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
|
||||
("""(define combine (lambda (f)
|
||||
(lambda (x y)
|
||||
(if (null? x) (quote ())
|
||||
(f (list (car x) (car y))
|
||||
((combine f) (cdr x) (cdr y)))))))""", None),
|
||||
("(define zip (combine cons))", None),
|
||||
("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
|
||||
("""(define riff-shuffle (lambda (deck)
|
||||
(begin
|
||||
(define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
|
||||
(define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
|
||||
(define mid (lambda (seq) (/ (length seq) 2)))
|
||||
((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None),
|
||||
("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
|
||||
("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]),
|
||||
("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]),
|
||||
])
|
||||
def test_evaluate(source: str, expected: Optional[Expression]) -> None:
|
||||
got = evaluate(parse(source), global_env_for_first_test)
|
||||
assert got == expected
|
||||
|
||||
|
||||
@fixture
|
||||
def std_env() -> Environment:
|
||||
return standard_env()
|
||||
|
||||
# tests for each of the cases in evaluate
|
||||
|
||||
def test_evaluate_variable() -> None:
|
||||
env: Environment = dict(x=10)
|
||||
source = 'x'
|
||||
expected = 10
|
||||
got = evaluate(parse(source), env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_literal(std_env: Environment) -> None:
|
||||
source = '3.3'
|
||||
expected = 3.3
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_quote(std_env: Environment) -> None:
|
||||
source = '(quote (1.1 is not 1))'
|
||||
expected = [1.1, 'is', 'not', 1]
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_if_true(std_env: Environment) -> None:
|
||||
source = '(if 1 10 no-such-thing)'
|
||||
expected = 10
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_if_false(std_env: Environment) -> None:
|
||||
source = '(if 0 no-such-thing 20)'
|
||||
expected = 20
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_define(std_env: Environment) -> None:
|
||||
source = '(define answer (* 6 7))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got is None
|
||||
assert std_env['answer'] == 42
|
||||
|
||||
|
||||
def test_lambda(std_env: Environment) -> None:
|
||||
source = '(lambda (a b) (if (>= a b) a b))'
|
||||
func = evaluate(parse(source), std_env)
|
||||
assert func.parms == ['a', 'b']
|
||||
assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
|
||||
assert func.env is std_env
|
||||
assert func(1, 2) == 2
|
||||
assert func(3, 2) == 3
|
||||
|
||||
|
||||
def test_begin(std_env: Environment) -> None:
|
||||
source = """
|
||||
(begin
|
||||
(define x (* 2 3))
|
||||
(* x 7)
|
||||
)
|
||||
"""
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 42
|
||||
|
||||
|
||||
def test_invocation_builtin_car(std_env: Environment) -> None:
|
||||
source = '(car (quote (11 22 33)))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 11
|
||||
|
||||
|
||||
def test_invocation_builtin_append(std_env: Environment) -> None:
|
||||
source = '(append (quote (a b)) (quote (c d)))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == ['a', 'b', 'c', 'd']
|
||||
|
||||
|
||||
def test_invocation_builtin_map(std_env: Environment) -> None:
|
||||
source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == [2, 4, 6]
|
||||
|
||||
|
||||
def test_invocation_user_procedure(std_env: Environment) -> None:
|
||||
source = """
|
||||
(begin
|
||||
(define max (lambda (a b) (if (>= a b) a b)))
|
||||
(max 22 11)
|
||||
)
|
||||
"""
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 22
|
@ -7,7 +7,7 @@ Demonstration of nested tuple unpacking::
|
||||
| latitude | longitude
|
||||
Mexico City | 19.4333 | -99.1333
|
||||
New York-Newark | 40.8086 | -74.0204
|
||||
Sao Paulo | -23.5478 | -46.6358
|
||||
São Paulo | -23.5478 | -46.6358
|
||||
|
||||
"""
|
||||
|
||||
@ -17,7 +17,7 @@ metro_areas = [
|
||||
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
|
||||
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
|
||||
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
|
||||
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
|
||||
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
|
||||
]
|
||||
|
||||
def main():
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
metro_lat_long.py
|
||||
metro_lat_lon.py
|
||||
|
||||
Demonstration of nested tuple unpacking::
|
||||
|
||||
@ -7,16 +7,17 @@ Demonstration of nested tuple unpacking::
|
||||
| latitude | longitude
|
||||
Mexico City | 19.4333 | -99.1333
|
||||
New York-Newark | 40.8086 | -74.0204
|
||||
Sao Paulo | -23.5478 | -46.6358
|
||||
São Paulo | -23.5478 | -46.6358
|
||||
|
||||
"""
|
||||
|
||||
# tag::MAIN[]
|
||||
metro_areas = [
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
|
||||
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
|
||||
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
|
||||
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
|
||||
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
|
||||
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
|
||||
]
|
||||
|
||||
def main():
|
||||
@ -27,3 +28,4 @@ def main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# end::MAIN[]
|
40
03-dict-set/py3.10/creator.py
Normal file
40
03-dict-set/py3.10/creator.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
Pattern matching with mapping—requires Python ≥ 3.10
|
||||
|
||||
# tag::DICT_MATCH_TEST[]
|
||||
>>> b1 = dict(api=1, author='Douglas Hofstadter',
|
||||
... type='book', title='Gödel, Escher, Bach')
|
||||
>>> get_creators(b1)
|
||||
['Douglas Hofstadter']
|
||||
>>> from collections import OrderedDict
|
||||
>>> b2 = OrderedDict(api=2, type='book',
|
||||
... title='Python in a Nutshell',
|
||||
... authors='Martelli Ravenscroft Holden'.split())
|
||||
>>> get_creators(b2)
|
||||
['Martelli', 'Ravenscroft', 'Holden']
|
||||
>>> get_creators({'type': 'book', 'pages': 770})
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}
|
||||
>>> get_creators('Spam, spam, spam')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Invalid record: 'Spam, spam, spam'
|
||||
|
||||
# end::DICT_MATCH_TEST[]
|
||||
"""
|
||||
|
||||
# tag::DICT_MATCH[]
|
||||
def get_creators(record: dict) -> list:
|
||||
match record:
|
||||
case {'type': 'book', 'api': 2, 'authors': [*names]}: # <1>
|
||||
return names
|
||||
case {'type': 'book', 'api': 1, 'author': name}: # <2>
|
||||
return [name]
|
||||
case {'type': 'book'}: # <3>
|
||||
raise ValueError(f"Invalid 'book' record: {record!r}")
|
||||
case {'type': 'movie', 'director': name}: # <4>
|
||||
return [name]
|
||||
case _: # <5>
|
||||
raise ValueError(f'Invalid record: {record!r}')
|
||||
# end::DICT_MATCH[]
|
@ -34,11 +34,11 @@ def main(args):
|
||||
print(count, 'characters shown')
|
||||
else:
|
||||
counts, firsts = category_stats()
|
||||
for cat, count in counts.most_common():
|
||||
for i, (cat, count) in enumerate(counts.most_common(), 1):
|
||||
first = firsts[cat]
|
||||
if cat == 'Cs':
|
||||
first = f'(surrogate U+{ord(first):04X})'
|
||||
print(f'{count:6} {cat} {first}')
|
||||
print(f'{i:2} {count:6} {cat} {first}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -8,9 +8,9 @@ print('sys.stdout.encoding:', sys.stdout.encoding)
|
||||
print()
|
||||
|
||||
test_chars = [
|
||||
'\u2026', # HORIZONTAL ELLIPSIS (in cp1252)
|
||||
'\u221E', # INFINITY (in cp437)
|
||||
'\u32B7', # CIRCLED NUMBER FORTY TWO
|
||||
'\N{HORIZONTAL ELLIPSIS}', # exists in cp1252, not in cp437
|
||||
'\N{INFINITY}', # exists in cp437, not in cp1252
|
||||
'\N{CIRCLED NUMBER FORTY TWO}', # not in cp437 or in cp1252
|
||||
]
|
||||
|
||||
for char in test_chars:
|
||||
|
@ -13,4 +13,4 @@ class Coordinate:
|
||||
self.lat = lat
|
||||
self.lon = lon
|
||||
|
||||
# end::COORDINATE[]
|
||||
# end::COORDINATE[]
|
92
05-data-classes/match_cities.py
Normal file
92
05-data-classes/match_cities.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""
|
||||
match_cities.py
|
||||
"""
|
||||
|
||||
# tag::CITY[]
|
||||
import typing
|
||||
|
||||
class City(typing.NamedTuple):
|
||||
continent: str
|
||||
name: str
|
||||
country: str
|
||||
|
||||
|
||||
cities = [
|
||||
City('Asia', 'Tokyo', 'JP'),
|
||||
City('Asia', 'Delhi', 'IN'),
|
||||
City('North America', 'Mexico City', 'MX'),
|
||||
City('North America', 'New York', 'US'),
|
||||
City('South America', 'São Paulo', 'BR'),
|
||||
]
|
||||
# end::CITY[]
|
||||
|
||||
# tag::ASIA[]
|
||||
def match_asian_cities():
|
||||
results = []
|
||||
for city in cities:
|
||||
match city:
|
||||
case City(continent='Asia'):
|
||||
results.append(city)
|
||||
return results
|
||||
# end::ASIA[]
|
||||
|
||||
# tag::ASIA_POSITIONAL[]
|
||||
def match_asian_cities_pos():
|
||||
results = []
|
||||
for city in cities:
|
||||
match city:
|
||||
case City('Asia'):
|
||||
results.append(city)
|
||||
return results
|
||||
# end::ASIA_POSITIONAL[]
|
||||
|
||||
|
||||
# tag::ASIA_COUNTRIES[]
|
||||
def match_asian_countries():
|
||||
results = []
|
||||
for city in cities:
|
||||
match city:
|
||||
case City(continent='Asia', country=cc):
|
||||
results.append(cc)
|
||||
return results
|
||||
# end::ASIA_COUNTRIES[]
|
||||
|
||||
# tag::ASIA_COUNTRIES_POSITIONAL[]
|
||||
def match_asian_countries_pos():
|
||||
results = []
|
||||
for city in cities:
|
||||
match city:
|
||||
case City('Asia', _, country):
|
||||
results.append(country)
|
||||
return results
|
||||
# end::ASIA_COUNTRIES_POSITIONAL[]
|
||||
|
||||
|
||||
def match_india():
|
||||
results = []
|
||||
for city in cities:
|
||||
match city:
|
||||
case City(_, name, 'IN'):
|
||||
results.append(name)
|
||||
return results
|
||||
|
||||
|
||||
def match_brazil():
|
||||
results = []
|
||||
for city in cities:
|
||||
match city:
|
||||
case City(country='BR', name=name):
|
||||
results.append(name)
|
||||
return results
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
tests = ((n, f) for n, f in globals().items() if n.startswith('match_'))
|
||||
|
||||
for name, func in tests:
|
||||
print(f'{name:15}\t{func()}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -17,4 +17,4 @@ class Coordinate(NamedTuple):
|
||||
lat: float # <1>
|
||||
lon: float
|
||||
reference: str = 'WGS84' # <2>
|
||||
# end::COORDINATE[]
|
||||
# end::COORDINATE[]
|
@ -5,5 +5,5 @@ class Coordinate(typing.NamedTuple):
|
||||
lat: float
|
||||
lon: float
|
||||
|
||||
trash = Coordinate('foo', None) # <1>
|
||||
trash = Coordinate('Ni!', None) # <1>
|
||||
print(trash)
|
@ -1,17 +0,0 @@
|
||||
To compile `metro_write.c` on MacOS 10.14 with XCode:
|
||||
|
||||
$ clang metro_write.c -o metro
|
||||
|
||||
Output:
|
||||
|
||||
$ xxd metro_areas.bin
|
||||
00000000: e207 0000 546f 6b79 6f00 0000 0000 0000 ....Tokyo.......
|
||||
00000010: 0000 0000 0000 0000 4a50 01e7 1158 274c ........JP...X'L
|
||||
00000020: df07 0000 5368 616e 6768 6169 0000 0000 ....Shanghai....
|
||||
00000030: 0100 0000 0e00 0000 434e 0000 f8a0 134c ........CN.....L
|
||||
00000040: df07 0000 4a61 6b61 7274 6100 0000 0000 ....Jakarta.....
|
||||
00000050: 0000 0000 0000 0000 4944 0000 bcc5 f14b ........ID.....K
|
||||
$ python3 metro_read.py
|
||||
2018 Tokyo, JP 43,868,228
|
||||
2015 Shanghai, CN 38,700,000
|
||||
2015 Jakarta, ID 31,689,592
|
Binary file not shown.
Binary file not shown.
@ -1,15 +0,0 @@
|
||||
from struct import iter_unpack
|
||||
|
||||
FORMAT = 'i12s2sf' # <1>
|
||||
|
||||
def text(field: bytes) -> str: # <2>
|
||||
octets = field.split(b'\0', 1)[0] # <3>
|
||||
return octets.decode('cp437') # <4>
|
||||
|
||||
with open('metro_areas.bin', 'rb') as fp: # <5>
|
||||
data = fp.read()
|
||||
|
||||
for fields in iter_unpack(FORMAT, data): # <6>
|
||||
year, name, country, pop = fields
|
||||
place = text(name) + ', ' + text(country) # <7>
|
||||
print(f'{year}\t{place}\t{pop:,.0f}')
|
@ -1,46 +0,0 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LEN 3
|
||||
|
||||
struct MetroArea {
|
||||
int year;
|
||||
char name[12];
|
||||
char country[2];
|
||||
float population;
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
struct MetroArea metro[LEN];
|
||||
int rank;
|
||||
|
||||
metro[0].year = 2018;
|
||||
strcpy(metro[0].name, "Tokyo");
|
||||
metro[0].country[0] = 'J';
|
||||
metro[0].country[1] = 'P';
|
||||
metro[0].population = 43868229.0;
|
||||
|
||||
metro[1].year = 2015;
|
||||
strcpy(metro[1].name, "Shanghai");
|
||||
metro[1].country[0] = 'C';
|
||||
metro[1].country[1] = 'N';
|
||||
metro[1].population = 38700000.0;
|
||||
|
||||
metro[2].year = 2015;
|
||||
strcpy(metro[2].name, "Jakarta");
|
||||
metro[2].country[0] = 'I';
|
||||
metro[2].country[1] = 'D';
|
||||
metro[2].population = 31689592.0;
|
||||
|
||||
FILE* data;
|
||||
if ( (data = fopen("metro_areas.bin", "wb")) == NULL ) {
|
||||
printf("Error opening file\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fwrite(metro, sizeof(struct MetroArea), LEN, data);
|
||||
fclose(data);
|
||||
|
||||
return 0;
|
||||
}
|
@ -15,22 +15,26 @@
|
||||
'banana'
|
||||
>>> clip('banana split', 12)
|
||||
'banana split'
|
||||
>>> clip('bananasplit', 5)
|
||||
'bananasplit'
|
||||
>>> clip('banana split', 8)
|
||||
'banana'
|
||||
"""
|
||||
|
||||
# tag::CLIP[]
|
||||
def clip(text, max_len=80):
|
||||
"""Return text clipped at the last space before or after max_len
|
||||
"""
|
||||
end = None
|
||||
if len(text) > max_len:
|
||||
space_before = text.rfind(' ', 0, max_len)
|
||||
if space_before >= 0:
|
||||
end = space_before
|
||||
else:
|
||||
space_after = text.rfind(' ', max_len)
|
||||
if space_after >= 0:
|
||||
end = space_after
|
||||
if end is None: # no spaces were found
|
||||
return text.rstrip()
|
||||
"""Return text clipped at the last space before or after max_len"""
|
||||
text = text.rstrip()
|
||||
end = len(text)
|
||||
if end <= max_len:
|
||||
return text
|
||||
space_before = text.rfind(' ', 0, max_len)
|
||||
if space_before >= 0:
|
||||
end = space_before
|
||||
else:
|
||||
space_after = text.find(' ', max_len)
|
||||
if space_after >= 0:
|
||||
end = space_after
|
||||
return text[:end].rstrip()
|
||||
# end::CLIP[]
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
>>> from clip import clip
|
||||
>>> from inspect import signature
|
||||
>>> sig = signature(clip)
|
||||
>>> sig # doctest: +ELLIPSIS
|
||||
<inspect.Signature object at 0x...>
|
||||
>>> sig
|
||||
<Signature (text, max_len=80)>
|
||||
>>> str(sig)
|
||||
'(text, max_len=80)'
|
||||
>>> for name, param in sig.parameters.items():
|
||||
|
@ -28,12 +28,9 @@ def tag(name, *content, class_=None, **attrs):
|
||||
"""Generate one or more HTML tags"""
|
||||
if class_ is not None:
|
||||
attrs['class'] = class_
|
||||
if attrs:
|
||||
attr_pairs = (f' {attr}="{value}"' for attr, value
|
||||
in sorted(attrs.items()))
|
||||
attr_str = ''.join(attr_pairs)
|
||||
else:
|
||||
attr_str = ''
|
||||
attr_pairs = (f' {attr}="{value}"' for attr, value
|
||||
in sorted(attrs.items()))
|
||||
attr_str = ''.join(attr_pairs)
|
||||
if content:
|
||||
elements = (f'<{name}{attr_str}>{c}</{name}>'
|
||||
for c in content)
|
||||
|
109
18-with-match/lisplus/examples_test.py
Normal file
109
18-with-match/lisplus/examples_test.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""
|
||||
Doctests for `parse`:
|
||||
|
||||
>>> from lis import parse
|
||||
|
||||
# tag::PARSE_DEMO[]
|
||||
>>> parse('1.5') # <1>
|
||||
1.5
|
||||
>>> parse('set!') # <2>
|
||||
'set!'
|
||||
>>> parse('(gcd 18 44)') # <3>
|
||||
['gcd', 18, 44]
|
||||
>>> parse('(- m (* n (// m n)))') # <4>
|
||||
['-', 'm', ['*', 'n', ['//', 'm', 'n']]]
|
||||
|
||||
# end::PARSE_DEMO[]
|
||||
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from lis import run
|
||||
|
||||
|
||||
fact_src = """
|
||||
(define (! n)
|
||||
(if (< n 2)
|
||||
1
|
||||
(* n (! (- n 1)))
|
||||
)
|
||||
)
|
||||
(! 42)
|
||||
"""
|
||||
def test_factorial():
|
||||
got = run(fact_src)
|
||||
assert got == 1405006117752879898543142606244511569936384000000000
|
||||
assert got == math.factorial(42)
|
||||
|
||||
|
||||
gcd_src = """
|
||||
(define (mod m n)
|
||||
(- m (* n (// m n))))
|
||||
(define (gcd m n)
|
||||
(if (= n 0)
|
||||
m
|
||||
(gcd n (mod m n))))
|
||||
(gcd 18 45)
|
||||
"""
|
||||
def test_gcd():
|
||||
got = run(gcd_src)
|
||||
assert got == 9
|
||||
|
||||
|
||||
quicksort_src = """
|
||||
(define (quicksort lst)
|
||||
(if (null? lst)
|
||||
lst
|
||||
(begin
|
||||
(define pivot (car lst))
|
||||
(define rest (cdr lst))
|
||||
(append
|
||||
(quicksort
|
||||
(filter (lambda (x) (< x pivot)) rest))
|
||||
(list pivot)
|
||||
(quicksort
|
||||
(filter (lambda (x) (>= x pivot)) rest)))
|
||||
)
|
||||
)
|
||||
)
|
||||
(quicksort (list 2 1 6 3 4 0 8 9 7 5))
|
||||
"""
|
||||
def test_quicksort():
|
||||
got = run(quicksort_src)
|
||||
assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
|
||||
|
||||
# Example from Structure and Interpretation of Computer Programs
|
||||
# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html
|
||||
|
||||
newton_src = """
|
||||
(define (sqrt x)
|
||||
(sqrt-iter 1.0 x))
|
||||
(define (sqrt-iter guess x)
|
||||
(if (good-enough? guess x)
|
||||
guess
|
||||
(sqrt-iter (improve guess x) x)))
|
||||
(define (good-enough? guess x)
|
||||
(< (abs (- (* guess guess) x)) 0.001))
|
||||
(define (improve guess x)
|
||||
(average guess (/ x guess)))
|
||||
(define (average x y)
|
||||
(/ (+ x y) 2))
|
||||
(sqrt 123454321)
|
||||
"""
|
||||
def test_newton():
|
||||
got = run(newton_src)
|
||||
assert math.isclose(got, 11111)
|
||||
|
||||
|
||||
closure_src = """
|
||||
(define (make-adder increment)
|
||||
(lambda (x) (+ increment x))
|
||||
)
|
||||
(define inc (make-adder 1))
|
||||
(inc 99)
|
||||
"""
|
||||
def test_newton():
|
||||
got = run(closure_src)
|
||||
assert got == 100
|
@ -23,7 +23,7 @@ Environment: TypeAlias = MutableMapping[Symbol, object]
|
||||
class Procedure:
|
||||
"A user-defined Scheme procedure."
|
||||
|
||||
def __init__(self, parms: list[Symbol], body: Expression, env: Environment):
|
||||
def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment):
|
||||
self.parms = parms
|
||||
self.body = body
|
||||
self.env = env
|
||||
@ -31,7 +31,9 @@ class Procedure:
|
||||
def __call__(self, *args: Expression) -> Any:
|
||||
local_env = dict(zip(self.parms, args))
|
||||
env: Environment = ChainMap(local_env, self.env)
|
||||
return evaluate(self.body, env)
|
||||
for exp in self.body:
|
||||
result = evaluate(exp, env)
|
||||
return result
|
||||
|
||||
|
||||
################ global environment
|
||||
@ -160,9 +162,9 @@ def evaluate(exp: Expression, env: Environment) -> Any:
|
||||
return evaluate(alternative, env)
|
||||
case ['define', Symbol(var), value_exp]:
|
||||
env[var] = evaluate(value_exp, env)
|
||||
case ['define', [Symbol(name), *parms], body]:
|
||||
case ['define', [Symbol(name), *parms], *body]:
|
||||
env[name] = Procedure(parms, body, env)
|
||||
case ['lambda', [*parms], body]:
|
||||
case ['lambda', [*parms], *body]:
|
||||
return Procedure(parms, body, env)
|
||||
case [op, *args]:
|
||||
proc = evaluate(op, env)
|
182
18-with-match/lisplus/lis_test.py
Normal file
182
18-with-match/lisplus/lis_test.py
Normal file
@ -0,0 +1,182 @@
|
||||
from typing import Optional
|
||||
|
||||
from pytest import mark, fixture
|
||||
|
||||
from lis import parse, evaluate, Expression, Environment, standard_env
|
||||
|
||||
############################################################# tests for parse
|
||||
|
||||
@mark.parametrize( 'source, expected', [
|
||||
('7', 7),
|
||||
('x', 'x'),
|
||||
('(sum 1 2 3)', ['sum', 1, 2, 3]),
|
||||
('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]),
|
||||
('99 100', 99), # parse stops at the first complete expression
|
||||
('(a)(b)', ['a']),
|
||||
])
|
||||
def test_parse(source: str, expected: Expression) -> None:
|
||||
got = parse(source)
|
||||
assert got == expected
|
||||
|
||||
|
||||
########################################################## tests for evaluate
|
||||
|
||||
# Norvig's tests are not isolated: they assume the
|
||||
# same environment from first to last test.
|
||||
global_env_for_first_test = standard_env()
|
||||
|
||||
@mark.parametrize( 'source, expected', [
|
||||
("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
|
||||
("(+ 2 2)", 4),
|
||||
("(+ (* 2 100) (* 1 10))", 210),
|
||||
("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
|
||||
("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
|
||||
("(define x 3)", None),
|
||||
("x", 3),
|
||||
("(+ x x)", 6),
|
||||
("((lambda (x) (+ x x)) 5)", 10),
|
||||
("(define twice (lambda (x) (* 2 x)))", None),
|
||||
("(twice 5)", 10),
|
||||
("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
|
||||
("((compose list twice) 5)", [10]),
|
||||
("(define repeat (lambda (f) (compose f f)))", None),
|
||||
("((repeat twice) 5)", 20),
|
||||
("((repeat (repeat twice)) 5)", 80),
|
||||
("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
|
||||
("(fact 3)", 6),
|
||||
("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000),
|
||||
("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
|
||||
("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
|
||||
("""(define combine (lambda (f)
|
||||
(lambda (x y)
|
||||
(if (null? x) (quote ())
|
||||
(f (list (car x) (car y))
|
||||
((combine f) (cdr x) (cdr y)))))))""", None),
|
||||
("(define zip (combine cons))", None),
|
||||
("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
|
||||
("""(define riff-shuffle (lambda (deck)
|
||||
(begin
|
||||
(define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
|
||||
(define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
|
||||
(define mid (lambda (seq) (/ (length seq) 2)))
|
||||
((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None),
|
||||
("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
|
||||
("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]),
|
||||
("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]),
|
||||
])
|
||||
def test_evaluate(source: str, expected: Optional[Expression]) -> None:
|
||||
got = evaluate(parse(source), global_env_for_first_test)
|
||||
assert got == expected
|
||||
|
||||
|
||||
@fixture
|
||||
def std_env() -> Environment:
|
||||
return standard_env()
|
||||
|
||||
# tests for each of the cases in evaluate
|
||||
|
||||
def test_evaluate_variable() -> None:
|
||||
env: Environment = dict(x=10)
|
||||
source = 'x'
|
||||
expected = 10
|
||||
got = evaluate(parse(source), env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_literal(std_env: Environment) -> None:
|
||||
source = '3.3'
|
||||
expected = 3.3
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_quote(std_env: Environment) -> None:
|
||||
source = '(quote (1.1 is not 1))'
|
||||
expected = [1.1, 'is', 'not', 1]
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_if_true(std_env: Environment) -> None:
|
||||
source = '(if 1 10 no-such-thing)'
|
||||
expected = 10
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_evaluate_if_false(std_env: Environment) -> None:
|
||||
source = '(if 0 no-such-thing 20)'
|
||||
expected = 20
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_define(std_env: Environment) -> None:
|
||||
source = '(define answer (* 6 7))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got is None
|
||||
assert std_env['answer'] == 42
|
||||
|
||||
|
||||
def test_lambda(std_env: Environment) -> None:
|
||||
source = '(lambda (a b) (if (>= a b) a b))'
|
||||
func = evaluate(parse(source), std_env)
|
||||
assert func.parms == ['a', 'b']
|
||||
assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
|
||||
assert func.env is std_env
|
||||
assert func(1, 2) == 2
|
||||
assert func(3, 2) == 3
|
||||
|
||||
|
||||
def test_begin(std_env: Environment) -> None:
|
||||
source = """
|
||||
(begin
|
||||
(define x (* 2 3))
|
||||
(* x 7)
|
||||
)
|
||||
"""
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 42
|
||||
|
||||
|
||||
def test_invocation_builtin_car(std_env: Environment) -> None:
|
||||
source = '(car (quote (11 22 33)))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 11
|
||||
|
||||
|
||||
def test_invocation_builtin_append(std_env: Environment) -> None:
|
||||
source = '(append (quote (a b)) (quote (c d)))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == ['a', 'b', 'c', 'd']
|
||||
|
||||
|
||||
def test_invocation_builtin_map(std_env: Environment) -> None:
|
||||
source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == [2, 4, 6]
|
||||
|
||||
|
||||
def test_invocation_user_procedure(std_env: Environment) -> None:
|
||||
source = """
|
||||
(begin
|
||||
(define max (lambda (a b) (if (>= a b) a b)))
|
||||
(max 22 11)
|
||||
)
|
||||
"""
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got == 22
|
||||
|
||||
|
||||
###################################### for py3.10/lis.py only
|
||||
|
||||
def test_define_function(std_env: Environment) -> None:
|
||||
source = '(define (max a b) (if (>= a b) a b))'
|
||||
got = evaluate(parse(source), std_env)
|
||||
assert got is None
|
||||
max_fn = std_env['max']
|
||||
assert max_fn.parms == ['a', 'b']
|
||||
assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']]
|
||||
assert max_fn.env is std_env
|
||||
assert max_fn(1, 2) == 2
|
||||
assert max_fn(3, 2) == 3
|
@ -59,7 +59,8 @@ Creating new attributes at runtime is restricted as well::
|
||||
...
|
||||
AttributeError: 'Movie' has no attribute 'director'
|
||||
|
||||
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
||||
The `_asdict` instance method creates a `dict` from the attributes
|
||||
of a `Movie` object::
|
||||
|
||||
>>> movie._asdict()
|
||||
{'title': 'The Godfather', 'year': 1972, 'box_office': 137.0}
|
||||
|
@ -58,7 +58,8 @@ Creating new attributes at runtime is restricted as well::
|
||||
...
|
||||
AttributeError: 'Movie' object has no attribute 'director'
|
||||
|
||||
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
||||
The `_asdict` instance method creates a `dict` from the attributes
|
||||
of a `Movie` object::
|
||||
|
||||
>>> movie._asdict()
|
||||
{'title': 'The Godfather', 'year': 1972, 'box_office': 137.0}
|
||||
|
@ -58,7 +58,8 @@ Creating new attributes at runtime is restricted as well::
|
||||
...
|
||||
AttributeError: 'Movie' object has no attribute 'director'
|
||||
|
||||
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
||||
The `_asdict` instance method creates a `dict` from the attributes
|
||||
of a `Movie` object::
|
||||
|
||||
>>> movie._asdict()
|
||||
{'title': 'The Godfather', 'year': 1972, 'box_office': 137.0}
|
||||
|
@ -26,7 +26,7 @@ Part / Chapter #|Title|Directory|1<sup>st</sup> ed. Chapter #
|
||||
2|An Array of Sequences|[02-array-seq](02-array-seq)|2
|
||||
3|Dictionaries and Sets|[03-dict-set](03-dict-set)|3
|
||||
4|Text versus Bytes|[04-text-byte](04-text-byte)|4
|
||||
5|Record-like Data Structures|[05-record-like](05-record-like)|🆕
|
||||
5|Record-like Data Structures|[05-data-classes](05-data-classes)|🆕
|
||||
6|Object References, Mutability, and Recycling|[06-obj-ref](06-obj-ref)|8
|
||||
**III – Functions as Objects**|
|
||||
7|First-Class Funcions|[07-1class-func](07-1class-func)|5
|
||||
@ -42,7 +42,7 @@ Part / Chapter #|Title|Directory|1<sup>st</sup> ed. Chapter #
|
||||
16|Operator Overloading: Doing It Right|[16-op-overloading](16-op-overloading)|13
|
||||
**V – Control Flow**|
|
||||
17|Iterables, Iterators, and Generators|[17-it-generator](17-it-generator)|14
|
||||
18|Context Managers and else Blocks|[18-context-mngr](18-context-mngr)|15
|
||||
18|Context Managers and else Blocks|[18-with-match](18-with-match)|15
|
||||
19|Classic Coroutines|[19-coroutine](19-coroutine)|16
|
||||
20|Concurrency Models in Python|[20-concurrency](20-concurrency)|🆕
|
||||
21|Concurrency with Futures|[21-futures](21-futures)|17
|
||||
|
Loading…
x
Reference in New Issue
Block a user