Modern JavaScript for AI
JavaScript from its beginning was a language that had first-class functions, just like lambda closures in Scheme and Lisp. But only relatively recently has the functional style of programming come to dominate JavaScript coding.
If your JavaScript looks like C code, these exercises will be quite eye-opening. Some references on modern JavaScript:
Using JavaScript Exercise Tester
To do the JavaScript exercises, download the CS325 JavaScript Exercise Tester code repository first. The best way to do this is with git clone, because then you can get updates with git pull.
To run the tests for an exercise set, start a local web server, as described in the tester README. The specifications for each exercise, including test cases, are given on the JS Tester web page that is displayed.
JavaScript Style
There are two popular style guides for JavaScript: Google and Airbnb. Use the one from Airbnb. It is pickier and more opinionated, but appears to be more popular.
Writing Clean JavaScript — ES6 Edition has many good tips, although some are worth discussing in class or on Piazza.
In this class, do not use the self-proclaimed misleadingly-named JavaScript Standard Style. For reasons why, see this discussion.
JS #1: Basic Exercises
Test sets: Do the tests labeled Conditionals, Loops, and Mapping in the tester.
Part of the sequence bundle
In the Code Critic, the Conditionals and Loops are one submission, and Mapping is a separate submission. Incorporate the inital feedback you get on Conditionals and Loops before submitting your Mapping solutions.
JS #2: Core JSON Matcher
Do not do these exercises until we have discussed in class how the Lisp matcher works, how binding lists are structured and updated non-destructively, why success is a list of binding list not just one binding list, how multiple binding lists can arise, and how pattern extensions can be added to the matcher without changing matcher source code.
Test sets: Your code needs to pass the tests labeled Match Primitives, Match Arrays, Match Objects, and Match Variables in the tester.
Define a general recursive matcher for JavaScript objects, analogous to the Lisp pattern matcher .
Defining JSON matching
Matching for JavaScript data is defined as follows:
- Primitive values, like numbers and strings, match if they are equal, as in ===, not ==.
- An array matches another array if the i'th element of the first array matches the i'th element of the second array.
- An object, as in JSON key-value objects, matches another object if the value of every key in the first object matches the corresponding key value in the second object.
- A variable in the pattern matches any object, if the variable is already bound to an equal value (see below), or the variable is unbound, in which case, the variable becomes bound to the object.
A variable is a string beginning with a question mark, such as "?x" or "?movie".
"Equal value" in programming is a complicated concept. Is
1 equal to 1.0? Is the array [1, 2]
equal to another array with the same elements in the same order?
Is the object {a: 1, b: 2}
equal to {b: 2, a: 1}? For this matcher, the answers should be yes.
This is the same as
the lodash function isEqual()
though you should not use that library.
Binding lists
To track variable bindings, keep a list of binding lists. Each binding list should be a JavaScript object with the variables as keys. For example,
[{ "?x": 1, "?y": ["a", "b"] }]
is a list of one binding list where the variable "?x" bound to 1 and the variable "?y" bound to the array ["a", "b"].
This means that
- match failure is the empty array; it is not
false,null, orundefined - match success is an array with at least one binding list
- the initial array of binding lists when matching starts is
[{}]
Note: in Lisp, mapcan is very useful for managing the list of binding lists. In JavaScript, the direct analogue is the array method flatMap().
Supporting extensibility
Almost every application of pattern-directed inference needs domain-specific pattern matching forms. For example, a system dealing with geographic data may need patterns to match locations that are "near" some location.
An extensible pattern matcher lets users add new pattern forms to a pattern matcher without editing the source of the pattern matcher. To implement this, you need some way to associate matching functions to pattern symbols, and define the matcher to call those functions when it sees those symbols.
To do this, include the following in your code for the core JSON matcher:
const matchers = new Object();
export const addMatchers = (newMatchers) => {
Object.assign(matchers, newMatchers);
};
const getMatcher = (pat, matchers) => {
const key = Object.keys(matchers).find(key => pat.hasOwnProperty(key));
return key ? matchers[key] : undefined
};
This defines a variable to hold an object whose keys will be pattern symbols and whose values will be matching functions. It defines getMatcher() to return the function for a key, if any, or undefined. Note that addMatchers() is exported.
Then, modify your match() function to call the function that getMatcher() returns, if there is one, passing it the pattern, the item being matched, and the binding lists. It's up to you to determine the appropriate place to call this code.
This code is not used for this exercise, but must be in place for subsequent matcher exercises.
Exporting the matcher
Put your core matcher in the file match-core.js.
Then import and export your definition of match()
in solutions.js like this:
import { match } from './match-core.js';
export { match };
Do not submit code for the other JSON matcher exercises until your core matcher code has been approved as done.
JS #4: Match Regular Expressions
Test set: Pass the tests labeled Match Regular Expressions in the tester.
The goal of this exercise is to make it possible to integrate
regular expressions as first-class citizens into the the structured patterns.
A simple example would be a pattern that matches a last, first name string
like "Smith, John" and binds the variables
?last and ?first appropriately.
Implement a matching subfunction for regular expressions so that matching
{ regexp: "(.*), *(.*)", pats: ["?last", "?first"] }
against the string "Smith, John" returns:
[{ "?last": "Smith", "?first": "John" }]
This exercise requires a basic understanding of regular expressions in JavaScript. In particular, you need to know what "capture groups" are, and how to get them in JavaScript when matching a regular expression to a string.
The normal rules for pattern match failure and adding
to bindings lists apply. For example, if ?last is already
bound to "Jones", the above match should fail.
Defining a pattern extension
The regular expression matcher should be defined as a
pattern extension. Create a new file, matchers.js.
At the top, put
import { match } from './match-core.js';
match() is imported because many pattern extensions call match() recursively. No other function from the core matcher should be imported.
Define a function that takes a pattern like
{ regexp: "(.*), *(.*)", pats: ["?last", "?first"] },
some item to match against, and a list of binding lists.
The function should return the updated bindings lists, or
[] if the match fails.
Export the function you defined using the pattern key as the name. For example, if you
called the function matchRegExp(), you'd write
export {
matchRegExp as regexp,
};
Then add the lines in italics to solutions.js to import the
extension:
import { match, addMatchers } from './match-core.js';
import * as matchers from './matchers.js';
addMatchers(matchers);
export { match };
import * as matchers creates
a JavaScript moduie that be accessed through
a module namespace object.
Calling addMatchers() adds the key-value pairs in
the module to tne internal matchers object. Note that only match()
is exported.
JS #5: Match AND, OR, NOT, SOME patterns
Test set: Pass the tests labeled Match And, Match Or, Match Not, and Match Some in the tester.
Implement pattern extensions to support logical pattern combination, like
?AND, ?OR, and ?NOT
in the Lisp matcher. All of these pattern forms are JavaScript objects.
All of these should be defined as extensions, just like
the regular expresssion matcher. They should not be defined
in the core matcher file nor have access to any matcher function
other than match(). It's simplest to define them in the
same file as the regular expression matcher.
AND and OR take a list of zero or more subpatterns:
{ and: [pat1, pat2, ...] }
{ or: [pat1, pat2, ...] }
An AND pattern matches an input if every subpattern matches that input. If a variable appears in more than one subpattern, it must match an equal value in each case.
For example
match({ and: ["?x", "?y"] }, "a", [{}])
would return
[{ "?x": "a", "?y": "a" }]
because the empty binding list can be extended with both bindings. But
match({and: [["?x", "?y"], ["?y", "?x"]]}, [1, 2])
would return:
[]
because you can't bind ?x and ?y to both 1 and 2.
An OR pattern matches an input for every subpattern matches that input. Each match independently adds bindings to the current binding lists, if possible.
For example
match({ or: ["?x", "?y"] }, "a", [{}])
would return a list of two binding lists:
[{ "?x": "a" }, { "?y": "a" }]
because the empty binding list can be extended with either binding. And
or({and: [["?x", "?y"], ["?y", "?x"]]}, [1, 2])
returns two bindings lists,
[{"?x": 1, "?y": 2}, {"?y": 1, "?x": 2}] [{"?x": 1, "?y": 2}, {"?y": 1, "?x": 2}]
But
match({ or: ["?x", "?y"] }, "a", [{ "?x": "b" }])
would return just one binding list:
[{ "?x": "b", "?y": "a" }]
because ?x can't be bound to a in the
given binding lists.
A NOT pattern takes one subpattern and matches an input if the subpattern does not match. No new bindings are created, but some binding lists may be eliminated. For example
match({ not: "?x" }, "a", [{ "?x": "a"}, {"?x": "b" }])
would return
[{ "?x": "b" }]
because that is the only binding that makes ?x not match a.
A SOME pattern takes one subpattern and matches it to an input array, returning the binding lists for every element that matches the subpattern.
For example
match({ some: "?x" }, ["a", "b", "c"])
would return
[ { "?x": "a" }, { "?x": "b" }, { "?x": "c" } ]
Note: The above examples use subpatterns that are just variables to keep things simple. But your code should allow subpatterns to be anything.
Exporting the extensions
Put your code in the file matchers.js where you
put the regular expression code. Export them under the names
needed for the patterns, e.g.,
export {
matchRegExp as regexp,
matchAnd as and,
...
}
Then the new extensions will be
exported and available for the tester with no change
to solutions.js needed.