-
Notifications
You must be signed in to change notification settings - Fork 3
Unpack Patterns
Khepri introduces syntax to unpack values while binding identifiers. Unpacks allow more concise code and can be used in function parameters lists, let expressions, and with statements.
- All unpacks produce zero or more immutable bindings.
- Unpack patterns effect the behavior of the generated code and have nothing to do with pattern matching. All effects happen at runtime.
- Unpacks take the immediate value of their target.
- For functions, parameter unpacks always take place before the body of the function is evaluated.
- Groups of unpacks are evaluated left to right.
Regular ECMAScript identifier name, binds an identifier to a value.
// Function using identifier patterns `a`, `b`, and `c`.
// These two are equivalent.
(\a b c -> [a, b, c])(1, 2); // [1,2,undefined]
(\a, b, c -> [a, b, c])(1, 2); // [1,2,undefined]
Unpacks a value by index using a set of grouped unpacks. Each sub unpack in an array pattern unpacks the target's value at the index where sub unpack appears.
var first = \[x] -> x;
// Commas are optional in array unpacks with multiple sub unpacks.
var add = \[x y] -> x + y;
var add = \[x, y] -> x + y;
add([1, 2]); // 3
No type checks are performed so it is possible to pass in array like objects or invalid objects:
var first = \[x] -> x;
first("abc"); // a
first({'0': 10, '1': 2}); // 10
first(null); // error, accessing null[0]
Patterns can be arbitrarily nested:
var dot2 = \[[a b] [x y]] -> a * x + b * y;
dot2([[1, 2], [3, 4]]); // 11
Object patterns generalize the array pattern for use with any string keys. Identifiers are bound to the unpacked value of the parameter's member for a given name at runtime.
// Unpack member 'a' to identifier a and member 'b' to identifier b.
var swapAB = \{'a': a, 'b': b} -> ({'a': b, 'b': a});
swapAB({'a': 3, 'b': 5}); // {'a': 5, 'b': 3};
Object patterns also can be nested:
var nested = \{'c': [x, {'value': y}]} -> x + y;
As shorthand, properties that have names that are valid identifiers can be unpacked directly by using the identifier as the key to unpack:
var swapAB = \{a b} -> ({'a': b, 'b': a});
Prefixing an object pattern with a ?
creates a checked object pattern. Checked object pattern use checked member accessors to ensure that the object being unpacked is a valid object.
// Normally, this would crash
var sum = \{a b} -> a + b;
sum(null); // crash accessing `null.a`;
// But checked accessors prevent the crash
var sum = \?{a b} -> a + b;
swapAB(null); // results in `null + null`
Checked object patterns implicitly propagate checks to all sub object patterns.
\?{a#{c#{d}} b#{e}} -> a + b + c + d + e;
// is the same as
\?{a#?{c#?{d}} b#?{e}} -> a + b + c + d + e;
The same syntax can be used to create checked array patterns:
var sum = \?[a b] -> a + b;
The as pattern unpacks a value to an identifier and then unpacks the value again with another pattern. This is similar to Haskell's @
. It can be used in any other pattern.
var dup = \arr#[x ...] -> [x, arr];
dup([1, [2]]); //[1, [1, [2]]];
Used as a top level in an object pattern, the AS pattern specifies the key to unpack:
var dup = \{x#{y}} -> y;
// is short for;
var dup = \{'x': {y}} -> y;
The ellipsis pattern may be used in array patterns and argument lists to unpack zero or more elements to an array.
var rest := \[x ...xs] -> xs;
rest [1, 2, 3]; // [2, 3];
rest [1, 2, 3, 4, 5]; // [2, 3, 4, 5];
The result of the ellipsis may collapse to contain zero elements:
rest [1]; // [];
rest []; // [];
Only one ellipsis pattern may appear per array pattern or argument list, but an arbitrary number of regular unpacks may be used both before and after an ellipsis pattern.
var mid := \[_ ...mid _] -> mid;
mid [1, 2, 3, 4, 5]; // [2, 3, 4]
Elements after the ellipsis pattern are taken relative to the end the element set. When the ellipsis pattern collapses to contain zero elements, the behavior is as if the ellipsis were omitted.
var first_and_last_two := \[x ...mid sl l] -> [x, sl, l];
first_and_last_two [1, 2, 3, 4, 5]; // [1, 4, 5];
// Collapsing ellipsis to zero, the unpack become `[x sl l]`
first_and_last_two [1, 2, 3]; // [1, 2, 3];
first_and_last_two [1, 2]; // [1, 2, undefined];
An ellipsis pattern may also be unnamed and be used for documentation purposes or to unpack values relative to the end of an array.
// Document that we take an arbitrary number of args.
var args := \a(...) -> a;
// Create relative unpacks
var sum_first_and_last := \[f (...) l] -> f + l;
var last := \[(...) l] -> l;
In general, slices generate a single call to [].prototype.slice
since the type of the argument cannot be determined at compile time.
// input (purposely prevent inlining)
var rest = \[x ...xs] -> { return xs; };
rest [1, 2, 3];
// generates
var rest = (function(__o) {
var xs = [].slice.call(__o, 1);
return xs;
});
rest([1, 2, 3]);
However, if the slice is known to target an array and additionally, for function parameter slices, that the function call can be inlined, no call to slice
is required.
// input
var rest = \[x ...xs] -> xs;
rest [1, 2, 3];
let [_ ...mid _] = [1, 2, 3, 4, 5]
in mid;
// output
var __o = [1, 2, 3],
xs = [__o[1], __o[2]];
xs;
var __o0 = [1, 2, 3, 4, 5],
mid = [__o0[1], __o0[2], __o0[3]];
mid;
Argument slices of inlined function calls will always use the lower overhead version:
var restArgs := \_ ...rest -> rest;
restArgs(1, 2, 3, 4, 5);
var __args = [1, 2, 3, 4, 5],
rest = [__args[1], __args[2], __args[3], __args[4]];
rest;