In my Kernel implementation Virtua, every object can potentially act as a left-hand side pattern.
A pattern responds to the message match(lhs, rhs, env) -- where the LHS is the pattern -- performs some computation, and possibly updates env with new bindings. The rules for Kernel's built-ins are:
- #ignore matches anything, and doesn't update the environment.
- Symbols match anything, and update the environment with a new binding from the symbol to the RHS.
- Nil matches only nil, and doesn't update the environment.
- Pairs match only pairs, and recursively match their car and cdr against the RHS's car and cdr, respectively.
- All other objects signal an error when used as a pattern.
With these simple rules one can write e.g. a lambda that destructures a list:
(apply (lambda (a b (c d)) (list a b c d))
(list 1 2 (list 3 4)))
==> (1 2 3 4)
(apply (lambda (a b (c d)) (list a b c d))
(list 1 2 (list 3 4)))
==> (1 2 3 4)
Now, why stop at these built-ins? Why not add first-class patterns? We only need some way to plug them into the parsing process...
An example: In Common Lisp, we use type declarations in method definitions:
(defmethod frobnicate ((a Foo) (b Bar)) ...)
(a Foo) means: match only instances of Foo, and bind them to the variable a.
Generalizing this, we could write:
(defmethod frobnicate ([<: a Foo] [<: b Bar]) ...)
The square brackets indicate first-class patterns, and in Lisp fashion, every first-class pattern has an operator, <: in this case. The parser simply calls <: with the operands list (a Foo), and this yields a first-class pattern, that performs a type check against Foo, and binds the variable a.
Another example would be an as pattern, like ML's alias patterns or Common Lisp's &whole, that lets us get a handle on a whole term, while also matching its subterms:
(lambda [as foo (a b c)] ...)
binds foo to the whole list of arguments, while binding a, b, and c to the first, second, and third element, respectively.
Another example would be destructuring of objects:
(lambda ([dest Person .name n .age a]) ...)
would be a function taking a single argument that must be an instance of Person, and binds n and a to its name and age, respectively.
With first-class patterns, all of these features can be added in luserland.
Another example would be destructuring of objects:
(lambda ([dest Person .name n .age a]) ...)
would be a function taking a single argument that must be an instance of Person, and binds n and a to its name and age, respectively.
With first-class patterns, all of these features can be added in luserland.
No comments:
Post a Comment