Object-oriented languages (including JavaScript) have primitive support for dynamic
dispatch, which automatically chooses the best method to invoke at run time, based on
the class of the receiver. You made use of this feature to good effect in
the Interpreter assignments. For example, the
eval
interp
.BinOp
AST node can simply invoke
this.e1.eval(…)
to recursively evaluate its first operand, without
having to manually figure out the right eval
method implementation for it;
rather, each kind of node just “knows” how to evaluate itself.
v
that is returned by the call this.e1.eval(…)
. To use
dynamic dispatch, our only option is to create a new method evalHelper
with implementations for each kind of value, and invoke v.evalHelper()
inside BinOp
’s implementation of eval
. This approach
is heavyweight and breaks up the logic of the original method in a potentially
unnatural way — so much so that it is common for programmers to instead resort
to manual dispatch through instanceof
tests.
5
).
Of course, one solution to this problem is to simply switch to Ha language that already supports pattern matching, like the one you implemented in Assignment 6. But changing languages is not always an option:
An increasingly popular solution to this problem is to provide the desired abstraction in the form of an internal domain-specific language (DSL). This name is important, so let’s break it down:
Internal DSLs (usually) don’t have their own syntax; instead they have a carefully-designed API in the host language that feels somewhat natural to write, and makes for readable code. Designing such an API is not easy, and some host languages are better than others. Smalltalk, Ruby, Haskell, and Scala are all reasonably good host languages. C not so much, though its macro system can be used to implement some limited kinds of DSLs. Java is basically awful as a host language.
So what should you look for in a host language?
aMatrix.times(anotherMatrix)
or
aMatrix * anotherMatrix
?
For Part II of Assignment 8, you’ll implement an internal DSL for pattern matching in JavaScript.
Here’s an example of how this DSL could be used to implement a zip
function, which turns two lists into a list of pairs, assuming we have declared
classes Nil
and Cons
to implement linked lists:
match
function
Our DSL consists of a match
function whose arguments are a value to be
matched, followed by zero or more (pattern, function) pairs:
match
will try to match the value with each of the patterns, in left-to-right
order. When it finds the first pattern that matches the value, match
will
call that pattern’s corresponding function and return the result of that call. If
none of the patterns match the value, match
will throw an exception like
this:
A pattern in our DSL serves two purposes:
length
property tells you the number of arguments it
expects. E.g., ((x, y) => x + y).length
evaluates to 2
.
Here is a list of the patterns that are supported in our DSL:
_
matches any value, and produces a single
binding that is equal to that value.
[
p1,
p2,
…,
pn]
will match any array whose
contents are matched by p1, p2, …,
pn, and if so, it will produce all of the bindings that were
produced by its sub-patterns, in order. For example:
|
evaluates to 12 .
|
|
throws a match failed exception because there is no value to be matched
by the second _ pattern.
|
|
throws a match failed exception because there is no pattern to match
the 7 .
|
pred(
f)
, where f is a
one-argument function, matches any value v for which
f(
v)
is
v
is truthy if v == true
.
isNumber
is defined as follows,
3
.
The class pattern instof(
C,
p1,
p2,
…,
pn)
, where
class
syntactic sugar.
instanceof
C is true
and
C.prototype.deconstruct.call(v)
matches the pattern
[
p1,
p2,
…,
pn]
.
On a successful match, a class pattern produces all of the bindings produced by
p1,
p2,
…,
pn, in order.
Every class should have a deconstruct
method that takes no arguments
and returns an array of values which constitute the “public view” of
an instance of the class. The default implementation of this method, in
“class” Object
, should return the empty array —
it is your responsibility to define this method. Other classes may
override deconstruct
. For instance, the deconstruct
method of Cons
in our example is defined as follows:
zip
method to recursively pattern match on the
structure of a list.
deconstruct
method gives the implementer of a class
control over how instances are exposed when they’re used in pattern matching.
Note that this view of the object does not have to correspond to its
representation. For instance, the author of a Point
class may choose to
represent points with x
and y
properties, but
deconstruct
each point as a pair of (angle, radius) values.
many(
p )
pattern, which
can only appear directly nested inside an array pattern. This pattern matches zero or
more values that are matched by p and produces k bindings, where
k is the number of bindings produced by matching against p. The
ith binding produced by many(
p )
is an array of the ith bindings produced from each of the matches
against p. For example:
|
evaluates to 10 , assuming the body of the function to the left sums
up the elements of the nums array.
|
|
evaluates to [[1, 3], [2, 4]] .
|
|
throws a match failed exception because the many(_) will
consume all of the elements of the array, and there will be no value to be
matched by the 'and' pattern.
|
|
does match, with xs bound to [1,2,3] and
x bound to 4 .
|
many
pattern ensures that the number of bindings produced can be
statically determined, independent of the number of values that end up matching the
pattern at run time. For example, the pattern [many([_,_])]
always
generates two bindings.*
many
pattern matches no values. Handling this case properly
may require some special support in your implementation.
42
, 'foo'
, and
undefined
— can be used as a literal pattern, i.e.,
a pattern that matches any value that is equal (===
) to it. Literal
patterns don’t produce any bindings.
Keep in mind that match
is just a regular JavaScript function, which means
that its arguments will be evaluated before any pattern matching actually happens. This
includes patterns like many(pred(isNumber))
: they are plain old JavaScript
expressions that will be evaluated to values before entering the match
function.
Please do your work in a file called match.js
. Each
time you refresh this page, that file is loaded by our test harness to run the unit
tests below. As in the previous assignments, you can add your own test cases by editing
tests.js
.
If you’re interested in pushing this project further, here are a few things you might like to try: