This is a pattern I have seen time and time again:
Enum Tree:
case LeafA(x: Int)
case LeafB(x: String)
case NodeC(x: Tree)
case NodeD(l: Tree, r: Tree)
def handle(t: Tree, value: SomeType) = t match
case t: LeafA => handleLeafA(t, value)
...
case t: NodeD => handleNodeD(t, value)
def handleLeafA(t: LeafA, value: SomeType) =
val LeafA(x) = t
...
...
def handleNodeD(t: NodeD, value: SomeType) =
val NodeD(left, right) = t
...
This usually happens when the cases of the pattern match become too large not to be their own functions
But this comes with big downsides, to keep the typing information and de-structuring, we have to repeat ourselves a lot, each case is repeated 5 times:
case t: LeafA=> handleLeafAdef handleLeafA(t: LeafA)val LeafA(x) =
Here is my proposed solution:
case parameters / pattern dispatch
The initial example would become:
Enum Tree:
case LeafA(x: Int)
case LeafB(x: String)
case NodeC(x: Tree)
case NodeD(l: Tree, r: Tree)
def handle(case LeafA(x), value: SomeType) =
...
...
def handle(case NodeD(left, right), value: SomeType) =
...
I’ll call synonyms (name TBD) methods which have the same signature except for case parameters
The compiler would take all synonyms and turn them into a single method which takes their supertype (which exactly?), whose body is a big match with every case:
def handle(t: Tree, value: SomeType) = t match
case LeafA(x) => // body of `def handle(case LeafA(x), value: SomeType)`
...
case NodeD(left, right) => // body of `def handle(case NodeD(left, right), value: SomeType)
One way to achieve something similar today is to make handle an abstract member on Tree, and override it in the children.
This has multiple downsides:
- The method must be defined by the authors, and cannot be added externally
- The method (or a pointer to it) is stored inside each instance, which can have performance overhead. (I’m not super sure of the details, but I remember discussing it with @bishabosha regarding scalac3)
- The method cannot be shared between similar concrete classes, unless additional levels are added (see also multi-level enums)
For the last point, here is an example of how it can be done with this proposal:
def countLeaves(case LeafA(_) | LeafB(_)): Int = 1
def countLeaves(case NodeC(t)): Int = countLeaves(t)
def countLeaves(case NodeD(l, r)): Int = countLeaves(l) + countLeaves(r)
Maybe we could add a definition statement, to reduce the need to repeat the return types and declare the type(s) on which we match:
/**
* Some Documentation here, in one centralized spot
*/
def countLeaves(match Tree): Int
def countLeaves(case LeafA(_) | LeafB(_)) = 1
def countLeaves(case NodeC(t)) = countLeaves(t) // No need to specify return type, even though recursive
def countLeaves(case NodeD(l, r)) = countLeaves(l) + countLeaves(r) // No need to specify return type, even though recursive
There is one big problem with this proposal: What happens when multiple patterns match ?
Intuitively, patterns should be matched in the definition order of the methods, but this means:
- Changing the order of defs can impact performance
- Changing the order of defs can change behaviour !
Alternative Proposal
Here is a simpler proposal which avoids this problem, but is not as powerful
Accept case patterns (without if filters) instead of parameters:
def handleLeafA(LeafA(x), value: SomeType) =
...
Which would be equivalent to handleLeafA from the original example
Does not do pattern dispatch
Parameter type is taken to be the extractor’s scrutinee type, in this case LeafA
This has the benefits of being more broadly useful, at the cost of being less useful in solving this specific problem, since you would still need to do
def handle(t: Tree, value: SomeType) = t match
case t: LeafA => handleLeafA(t, value)
...
case t: NodeD => handleNodeD(t, value)