cs242作业1记录
Part 1: Implement SKI Interpreter
SKI Expr definition:
Property:
src/ski.py
defines the expressions in SKI.
In our Python framework, the combinators S, K, and I are represented as
S()
,K()
, andI()
, a variable x asVar("x")
, and an application e1 e2 asApp(e1,e2)
To check whether an expression e is, for example, the K combinator, you can use
isinstance(e, K)
which returns True if e is indeed the K combinator, and False otherwise.cd to
f19hw01
, runsh testski.sh
to test the output
Rules & Definitions
A valid expression conforms to BNF and can be defined recursively.
Take an example to show the usage of the definition of SKI:
1 | # e = (((S x) y) z) |
Implementation
Simple rule: rewrite until cannot rewrite, i.e. previous expr is the same as current expr:
1 | def eval(e: ski.Expr) -> ski.Expr: |
Rewrite
We only rewrite applications (expressions of the form S x y z | K x y | I x), treating variables as terminals.
We then recursively rewrite the two components of the expression (e.e1 and e.e2), as every expression is composed of two subexpressions. For example, (S x y z) is represented as (((S x) y) z).
Method of listing cases: $S\ K\ I$
If $I$, then
r_expr = e.e2
(forI x = x
andI
ise.e1
);If $K$, then
r_expr = e.e1.e2
(for(K x) y
and(K x)
ise.e1
)if $S$, then the rewritten expression is: Apply
e.e1.e1.e2
ande.e1.e1
toe.e2
Conditions
We NEED to varify the specific combinator, and corresponding to rewritten rules, i.e. how we rewrite, how we check (On associativity aspect).
Here we name the expression taken in as app
, indicating that it is an expression which follows the Application rule.
I x
is quite obvious:isinstance(app.e1, ski.I)
K x y
equivalent to(K x) y
(We followed this associativity when rewriting) , so just check thee.e1 is ski.App
ande.e1.e1 is ski.K
S x y z
evaluated as(((S x) y) z)
, just like description above
Drawing a tree diagram may help:
1 | """ |
Part2: Programming in SKI calculus
Implement operations with SKI combinator: or
, not
, is_odd
. Notice that True and False are already implemented as tt
and ff
, all of them are higer-order functions.
Example:
1 | or tt ff x y = x |
Unlike traditional programming, which often requires returning specific variables, this problem primarily focuses on achieving correct behavior without strictly adhering to the given definition. In other words, an expression is considered correct if it exhibits the expected behavior, such as taking two arguments and returning the first when the result is true.
Now let’s analyse the concrete behaviour:
or
—-Since True is defined to return its first argument, the OR function can be implemented by returning the first argument if it is True, and the second argument otherwise. This correctly models the behavior of the logical OR operation.and
—-Similarly to OR, the AND function returns the second argument if it’s false, and the first argument otherwise.not
—-Trivial.
Part3: Array Programming with NumPy
Combinator calculi such as SKI have influenced the design of languages that emphasize using primitive recursive combinators in “whole data type” operations.
1 | def find_missing(n: int, arr: np.ndarray) -> np.ndarray: |
It’s noteworthy that the solution above employs the .
symbol. This functional composition style, where functions are chained together using the .
operator, bears a strong resemblance to Haskell. Both languages, influenced by SKI calculus, approach problem-solving through a data-processing pipeline paradigm.