Hi all,
It might be useful to try to retrace the fundamental structure of pattern matching related language constructs. I think the core is the following two-step process:
1. Specify a situation, including a choice of named locations in that situation.
2. Check whether that situation is present, and if so: bind values to names accordingly.
We may then run code where those names are in scope.
An SML function definition is a typical case. Here, step 1 occurs at compile time and step 2 at runtime. We'd expect help from the compiler checking that we have lined up a complete (but possible somewhat redundant) set of alternative situation specifications, such that it is guaranteed that execution will not be stuck at runtime. We will choose the (textually) first specification to which the given runtime situation conforms, bind the names to values accordingly, and run the associated code:
fun sum [] = 0
| sum [x] = x (* redundant, but not unused *)
| sum (x::xs) = x + sum xs;
With algebraic datatypes, lack of subtyping, and names that stand for a value rather than standing for a storage location (i.e., immutable rather than mutable variables), we can use inference to find the best possible typing for each name, so there is rarely a need to declare the types of these names. This is a quite wholesome design. But we cannot use it directly, because we have mutability, subtyping, and OO encapsulation.
Views (see the paper mentioned by Paul) will allow us to create an arbitrary isomorphism between a representation type and a 'view type' (so if we want to view a primitive int value as a Peano 'Zero | Succ n' style inductive type, we can define functions to take us from one to the other, unambiguously). The property that such a pair of functions is _actually_ a pair of exactly inverse functions is left unchecked, the programmer of such functions must be careful, and everybody else must trust or check that it is correct.
In the slightly more general setting that I've outlined with the steps 1 and 2 above, views allow us to specify situations in such a way that an arbitrary (encapsulated) representation can be tested as being isomorphic (or not) to a given specification, and in that case also which values we find in specific locations inside the view type value, such that we can bind names to values.
But I have the impression that views require strict static typing, because it relies crucially on compilers adding applications of the isomorphism (mapping values from the view type to the representation type, and vice versa) at locations where the view type is expected and the representation type is encountered, and vice versa.
That just doesn't work for Dart, where it is a core value that programs must be able to run independently of any type annotations (and, in particular, we cannot require that type annotations are present or even consistent). NB: This is a feature, not a bug! ;-D
The other thing that doesn't work out of the box is the choice to bind _values_ to names in the view type domain. With mutable state, we'd get a weird, half-baked type of access to the actual entities in question when we can just read their values at the beginning of a scope: We need access that corresponds to the nature of the underlying entities, e.g., such that we can mutate a getter/setter pair (say, a field).
Hence, to me, the obvious mechanism corresponding to functional style pattern matching for an OO language with no requirement for strict static typing is a wrapping mechanism:
1. Specify an excerpt of an object graph and a 'wrapper' class that provides an interface for us to work with the objects in that object graph
2. Check whether a "matching" object graph exists, and if so create and initialize a wrapper object for it.
Applying that idea to the given deconstruction proposal is a separate (presumably long and detailed) discussion, but one simple idea could be as follows:
A variant of list literals may be used to specify an object graph consisting of a list and a number of elements in that list, and we can specify properties of each element using declaration syntax:
var [a, b] = myExpressionExpectedToDeliverAList;
// corresponds to
List freshName = myExpressionExpectedToDeliverAList;
assert(freshName.length == 2);
var a = freshName[0];
var b = freshName[1];
var <num>[int a, double b, ...] = myExpression;
// corresponds to
List<num> freshName = myExpression;
int a = freshName[0];
double b = freshName[1];
For user-defined classes (where no special literals are present in the syntax), we could use constructor calls with declarations as arguments:
var Pair(int x, int y) = someExpression;
// corresponds to
Pair freshName = someExpression;
int x = freshName.computeConstructorArgOneFor_int_int;
int y = freshName.computeConstructorArgTwoFor_int_int;
We would need to come up with a sufficiently useful and flexible way to declare how to compute the "pattern matching constructor arguments". Presumably the class Point would have to be written in such a way that it passes a certain compile time check a la "this class knows how to match an (int, int) constructor argument list", or maybe just ".. a constructor argument list of length two" (adjust the name to "computeConstructorArg...ForTwoArgs"), and the corresponding details of how to generate code for "computeConstructorArg..." would be well-defined when the class passes that check.
This is a quite restricted idea because it only allows us to specify a situation (a partial object graph) which is constituted by a single object (a List, a Pair, etc.), but it does let us specify a view-like relation (it is encapsulated in the Pair class how to obtain a suitable 'x' and 'y' from a given Pair).
With a more general model, we could specify object graphs like "we must have a Tree, call it t, and there must be a path along 'left' and 'right' references to another Tree, t2, such that t2.value == 142", and then initialize 'Tree t' and 'Tree t2' in case that situation exists. When that situation does not exist, it would be nice to be able to specify an "else" specification, rather than crashing with a "pattern match failed", but that shouldn't be so hard. Ah, let's add a wild guess at some syntax, too:
if (Tree t where t2 == t.[left|right]* && t2.value == 142 = myTreeExpression) {
// Use t and t2.
} else {
Tree t = myTreeExpression;
// Use t, possibly exploiting the knowledge that it does not contain 142
}
Don't try this at home, though. ;-)