diff --git a/02-array-seq/lispy/py3.10/examples_test.py b/02-array-seq/lispy/py3.10/examples_test.py new file mode 100644 index 0000000..ba6bd10 --- /dev/null +++ b/02-array-seq/lispy/py3.10/examples_test.py @@ -0,0 +1,258 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] + +""" + +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)))) +(display (gcd 18 45)) +""" +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' + + +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_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] diff --git a/02-array-seq/lispy/py3.10/lis.py b/02-array-seq/lispy/py3.10/lis.py new file mode 100755 index 0000000..150fca7 --- /dev/null +++ b/02-array-seq/lispy/py3.10/lis.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python + +################ 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 + +# tag::IMPORTS[] +import math +import operator as op +from collections import ChainMap +from itertools import chain +from typing import Any, TypeAlias, NoReturn + +Symbol: TypeAlias = str +Atom: TypeAlias = float | int | Symbol +Expression: TypeAlias = Atom | list +# end::IMPORTS[] + + +################ 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) + + +################ Global Environment + +# tag::ENV_CLASS[] +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value + return + raise KeyError(key) +# end::ENV_CLASS[] + + +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, + 'display': lambda x: print(lispstr(x)), + '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 + + +################ Interaction: A REPL + +# tag::REPL[] +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = Environment({}, standard_env()) + while True: + ast = parse(input(prompt)) + val = evaluate(ast, 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) +# end::REPL[] + + +################ Evaluator + +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] + +# tag::EVAL_MATCH_TOP[] +def evaluate(exp: Expression, env: Environment) -> Any: + "Evaluate an expression in an environment." + match exp: +# end::EVAL_MATCH_TOP[] + case int(x) | float(x): + return x + case Symbol(name): + return env[name] +# tag::EVAL_MATCH_MIDDLE[] + case ['quote', x]: # <1> + return x + case ['if', test, consequence, alternative]: # <2> + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + case ['lambda', [*parms], *body] if body: # <3> + return Procedure(parms, body, env) + case ['define', Symbol(name), value_exp]: # <4> + env[name] = evaluate(value_exp, env) +# end::EVAL_MATCH_MIDDLE[] + case ['define', [Symbol(name), *parms], *body] if body: + env[name] = Procedure(parms, body, env) + case ['set!', Symbol(name), value_exp]: + env.change(name, evaluate(value_exp, env)) + case [func_exp, *args] if func_exp not in KEYWORDS: + proc = evaluate(func_exp, env) + values = [evaluate(arg, env) for arg in args] + return proc(*values) +# tag::EVAL_MATCH_BOTTOM[] + case _: # <5> + raise SyntaxError(lispstr(exp)) +# end::EVAL_MATCH_BOTTOM[] + +# tag::PROCEDURE[] +class Procedure: + "A user-defined Scheme procedure." + + def __init__( # <1> + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms # <2> + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: # <3> + local_env = dict(zip(self.parms, args)) # <4> + env = Environment(local_env, self.env) # <5> + for exp in self.body: # <6> + result = evaluate(exp, env) + return result # <7> +# end::PROCEDURE[] + + +################ command-line interface + +def run(source: str) -> Any: + global_env = Environment({}, standard_env()) + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.10/lis_test.py b/02-array-seq/lispy/py3.10/lis_test.py new file mode 100644 index 0000000..03dc15c --- /dev/null +++ b/02-array-seq/lispy/py3.10/lis_test.py @@ -0,0 +1,180 @@ +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 cases in evaluate + +def test_evaluate_variable() -> None: + env = Environment({'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 + + +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 diff --git a/02-array-seq/lispy/py3.10/quicksort.scm b/02-array-seq/lispy/py3.10/quicksort.scm new file mode 100644 index 0000000..08dd596 --- /dev/null +++ b/02-array-seq/lispy/py3.10/quicksort.scm @@ -0,0 +1,17 @@ +(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))) + ) + ) +) +(display + (quicksort (list 2 1 6 3 4 0 8 9 7 5))) diff --git a/02-array-seq/lispy/py3.9/README.md b/02-array-seq/lispy/py3.9/README.md new file mode 100644 index 0000000..fdb1f08 --- /dev/null +++ b/02-array-seq/lispy/py3.9/README.md @@ -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](https://web.mit.edu/scheme_v9.2/doc/mit-scheme-ref/Lambda-Expressions.html), 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 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`. + diff --git a/02-array-seq/lispy/py3.9/examples_test.py b/02-array-seq/lispy/py3.9/examples_test.py new file mode 100644 index 0000000..bdcda48 --- /dev/null +++ b/02-array-seq/lispy/py3.9/examples_test.py @@ -0,0 +1,196 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define % (lambda (a b) (* (/ a b) 100)))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define ! + (lambda (n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + +closure_src = """ +(define make-adder + (lambda (increment) + (lambda (x) (+ increment x)) + ) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define make-counter + (lambda () + (define n 0) + (lambda () + (set! n (+ n 1)) + n) + ) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define make-averager + (lambda () + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] \ No newline at end of file diff --git a/02-array-seq/lispy/py3.9/lis.py b/02-array-seq/lispy/py3.9/lis.py new file mode 100644 index 0000000..201b41e --- /dev/null +++ b/02-array-seq/lispy/py3.9/lis.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python + +################ 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 itertools import chain +from typing import Any, Union, NoReturn + +Symbol = str +Atom = Union[float, int, Symbol] +Expression = Union[Atom, list] + + +################ 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) + + +################ Global Environment + +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value # type: ignore[index] + return + raise KeyError(key) + + +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, + 'display': lambda x: print(lispstr(x)), + '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 + + +################ Interaction: A REPL + +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = Environment({}, standard_env()) + while True: + ast = parse(input(prompt)) + val = evaluate(ast, 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) + + +################ Evaluator + +# tag::EVAL_IF_TOP[] +def evaluate(exp: Expression, env: Environment) -> Any: + "Evaluate an expression in an environment." + if isinstance(exp, Symbol): # variable reference + return env[exp] +# end::EVAL_IF_TOP[] + elif not isinstance(exp, list): # constant literal + return exp +# tag::EVAL_IF_MIDDLE[] + elif exp[0] == 'quote': # (quote exp) + (_, x) = exp + return x + elif exp[0] == 'if': # (if test conseq alt) + (_, test, consequence, alternative) = exp + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + elif exp[0] == 'lambda': # (lambda (parm…) body…) + (_, parms, *body) = exp + return Procedure(parms, body, env) + elif exp[0] == 'define': + (_, name, value_exp) = exp + env[name] = evaluate(value_exp, env) +# end::EVAL_IF_MIDDLE[] + elif exp[0] == 'set!': + (_, name, value_exp) = exp + env.change(name, evaluate(value_exp, env)) + else: # (proc arg…) + (func_exp, *args) = exp + proc = evaluate(func_exp, env) + args = [evaluate(arg, env) for arg in args] + return proc(*args) + + +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(local_env, self.env) + for exp in self.body: + result = evaluate(exp, env) + return result + + +################ command-line interface + +def run(source: str) -> Any: + global_env = Environment({}, standard_env()) + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.9/lis_test.py b/02-array-seq/lispy/py3.9/lis_test.py new file mode 100644 index 0000000..64bafb4 --- /dev/null +++ b/02-array-seq/lispy/py3.9/lis_test.py @@ -0,0 +1,180 @@ +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 cases in evaluate + +def test_evaluate_variable() -> None: + env = Environment({'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 + + +def test_define_function(std_env: Environment) -> None: + source = '(define max (lambda (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 diff --git a/18-with-match/lispy/py3.10/examples_test.py b/18-with-match/lispy/py3.10/examples_test.py index ba6bd10..2807596 100644 --- a/18-with-match/lispy/py3.10/examples_test.py +++ b/18-with-match/lispy/py3.10/examples_test.py @@ -48,7 +48,6 @@ Doctests for `evaluate` # end::EVAL_NUMBER[] # tag::EVAL_SYMBOL[] ->>> from lis import standard_env >>> evaluate(parse('+'), standard_env()) >>> evaluate(parse('ni!'), standard_env()) diff --git a/18-with-match/lispy/py3.10/lis.py b/18-with-match/lispy/py3.10/lis.py index ff494d1..30ad8a8 100755 --- a/18-with-match/lispy/py3.10/lis.py +++ b/18-with-match/lispy/py3.10/lis.py @@ -74,7 +74,6 @@ class Environment(ChainMap[Symbol, Any]): raise KeyError(key) # end::ENV_CLASS[] - def standard_env() -> Environment: "An environment with some Scheme standard procedures." env = Environment() @@ -122,7 +121,7 @@ def standard_env() -> Environment: # tag::REPL[] def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: ast = parse(input(prompt)) val = evaluate(ast, global_env) @@ -141,10 +140,7 @@ def lispstr(exp: object) -> str: ################ Evaluator # tag::EVALUATE[] -KEYWORDS = {'quote', 'if', 'lambda', 'define', 'set!'} - -def is_keyword(s: Any) -> bool: - return isinstance(s, Symbol) and s in KEYWORDS +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] def evaluate(exp: Expression, env: Environment) -> Any: "Evaluate an expression in an environment." @@ -162,13 +158,13 @@ def evaluate(exp: Expression, env: Environment) -> Any: return evaluate(alternative, env) case ['lambda', [*parms], *body] if body: return Procedure(parms, body, env) - case ['define', Symbol(var), value_exp]: - env[var] = evaluate(value_exp, env) + case ['define', Symbol(name), value_exp]: + env[name] = evaluate(value_exp, env) case ['define', [Symbol(name), *parms], *body] if body: env[name] = Procedure(parms, body, env) - case ['set!', Symbol(var), value_exp]: - env.change(var, evaluate(value_exp, env)) - case [func_exp, *args] if not is_keyword(func_exp): + case ['set!', Symbol(name), value_exp]: + env.change(name, evaluate(value_exp, env)) + case [func_exp, *args] if func_exp not in KEYWORDS: proc = evaluate(func_exp, env) values = [evaluate(arg, env) for arg in args] return proc(*values) @@ -199,7 +195,7 @@ class Procedure: ################ command-line interface def run(source: str) -> Any: - global_env = standard_env() + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) diff --git a/18-with-match/lispy/py3.9/lis.py b/18-with-match/lispy/py3.9/lis.py index e8881cd..6ab8a19 100644 --- a/18-with-match/lispy/py3.9/lis.py +++ b/18-with-match/lispy/py3.9/lis.py @@ -117,7 +117,7 @@ def standard_env() -> Environment: def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: ast = parse(input(prompt)) val = evaluate(ast, global_env) @@ -193,7 +193,7 @@ class Procedure: ################ command-line interface def run(source: str) -> Any: - global_env = standard_env() + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) diff --git a/18-with-match/mirror_gen.py b/18-with-match/mirror_gen.py index 6dfaf02..2758045 100644 --- a/18-with-match/mirror_gen.py +++ b/18-with-match/mirror_gen.py @@ -15,6 +15,9 @@ While active, the context manager reverses text output to YKCOWREBBAJ >>> what 'JABBERWOCKY' + >>> print('back to normal') + back to normal + # end::MIRROR_GEN_DEMO_1[] diff --git a/19-concurrency/primes/spinner_prime_async_nap.py b/19-concurrency/primes/spinner_prime_async_nap.py index 4d6fa47..b6e6518 100644 --- a/19-concurrency/primes/spinner_prime_async_nap.py +++ b/19-concurrency/primes/spinner_prime_async_nap.py @@ -7,6 +7,7 @@ import asyncio import itertools import math +import functools # tag::PRIME_NAP[] async def is_prime(n): @@ -21,8 +22,8 @@ async def is_prime(n): for i in range(3, root + 1, 2): if n % i == 0: return False - if i % 100_000 == 1: # <2> - await asyncio.sleep(0) + if i % 100_000 == 1: + await asyncio.sleep(0) # <1> return True # end::PRIME_NAP[] @@ -39,13 +40,13 @@ async def spin(msg: str) -> None: print(f'\r{blanks}\r', end='') async def check(n: int) -> int: - return await is_prime(n) # <4> + return await is_prime(n) async def supervisor(n: int) -> int: - spinner = asyncio.create_task(spin('thinking!')) # <1> - print('spinner object:', spinner) # <2> - result = await check(n) # <3> - spinner.cancel() # <5> + spinner = asyncio.create_task(spin('thinking!')) + print('spinner object:', spinner) + result = await check(n) + spinner.cancel() return result def main() -> None: diff --git a/22-dyn-attr-prop/doc_property.py b/22-dyn-attr-prop/doc_property.py index b1ba351..5b653ef 100644 --- a/22-dyn-attr-prop/doc_property.py +++ b/22-dyn-attr-prop/doc_property.py @@ -14,7 +14,7 @@ class Foo: @property def bar(self): - '''The bar attribute''' + """The bar attribute""" return self.__dict__['bar'] @bar.setter