sync with O'Reilly Atlas

This commit is contained in:
Luciano Ramalho
2021-07-07 23:45:54 -03:00
parent f0f160844d
commit 23e78eeb82
64 changed files with 2087 additions and 124 deletions

View 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`.

View 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)

View 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