This snippet demonstrates how the Fantom switch statement can be used to perform pattern matching logic:
switch (e) {
case UnOp( "-", UnOp("-", null) ):
return ((e as UnOp).arg as UnOp).arg
case BinOp( "+", null, Number( 0 ) ):
return (e as BinOp).left
case BinOp( "*", null, Number( 1 ) ):
return (e as BinOp).left
default: return e
}
The first branch demonstrates a recursive pattern. null acts a the wildcard value here. The matching is performed on the type and value of the fields of the objects.
It works by constructing objects for the patterns, and a special equals method which compares the objects recursively using reflection, ignoring fields that have a wildcard value, or are annotated with the PatternExclude facet. The wildcard value can be set with the PatternWildcard facet.
This looks pretty ok in Fantom, the main ugliness is that you can not bind variables of the right type in the branches so you have to cast with as.
Other disadvantage are that new objects are created each run, and that you need this special equals method.
ivanTue 31 Jul 2012
Interesting example, thanks!
I also wanted to take more power out of switch, but couple of times I found that it is interesting to use pattern like this (just to illustrate an idea):
This has the advantage that the objects do not need a special equals, but there is more syntactic noise.
brianWed 1 Aug 2012
BTW, I would like to see our switch statement enhanced. Proposals that don't require changes to existing type system are welcome.
ivanThu 2 Aug 2012
Probably ability to inverse equality order? Like new switchr statement, which calls caseN.equals(input) instead of input.equals(caseN)? This would allow to use case objects which override equals method to perform sort of matching.
SlimerDudeThu 2 Aug 2012
Doesn't that break the equals contract? Surely:
caseN.equals(input) == input.equals(caseN)
should always be true?
ivanThu 2 Aug 2012
It does. Probably more integrate solution is like this:
mixin Matcher { abstract Bool matches(Obj? input) }
// ....
match(input) {
case mymatcher1:
break;
case mymatcher2:
break;
}
heliumThu 2 Aug 2012
I agree with SlimerDude. Instead of calling equals it should perhaps call a method called matches if it's available and only fall back to the current equals test if not.
brianThu 2 Aug 2012
Having switch/case look for match instead of equals seems elegant. So what all would that buy us?
dobesvFri 3 Aug 2012
Hmm it seems like a halfway solution, though.
Maybe something based on it-blocks would be more sophisticated? An alternative proposal:
switch(x) {
case it is Str && it.length() == 3: ...
case it is Str && length > 3, len:=length: ...
case it is Point, x:=x, y:=y: ...
}
The value after "case" is parsed an expression with it=the switch parameter and using it in scope (as with an it block).
After the condition, a comma-seperated list of assignments create locally scoped variables whose initializers are also invoked in an it-block of sorts
As a special case the use of "it is" or just "is" may check and set the type of "it" for the rest of the expression so you don't have to put any casts in your condition.
The "it-block" style initialization could be handy for unpacking as well:
unpack foo.bar() as a:=a, b:=b;
It's not as compact as you might dream of but it's pretty good. I think it might be better than creating special objects to match against with null checking logic.
SlimerDudeSat 4 Aug 2012
I'm quite surprised by about the amount of enthusiasm there is over the switch statement. For I've always just seen it as a type of poor man's polymorphism, a throw back to the old days, and very rarely use it myself.
@Dobesv: Your idea of using it is quite nice but I wouldn't be keen having new language features introduced for the sake of one keyword e.g. the is and comma. Plus, as it's in the interests of readability, I get the feeling you're trying to push the language to do what a programmatic solution would do better. As per your example, the following looks far more readable and due to method names, shows more intent also:
class Wotever {
static Void main(Str[] args) {
x := "wot" // "wotever"
switchObj := SwitchObj(x)
switch (true) {
case switchObj.case1:
echo("case1 stuff")
case switchObj.case2:
echo("case2 stuff")
}
}
}
internal class SwitchObj {
Obj x
new make(Obj x) { this.x = x }
Bool case1() {
(x.typeof == Str#) && (x as Str).size == 3
}
Bool case2() {
(x.typeof == Str#) && (x as Str).size > 3
}
}
heliumSat 4 Aug 2012
How is this more readable? The functionality is spread over several classes. To follow the flow I have to jump around between different places.
I'm quite surprised by about the amount of enthusiasm there is over the switch statement. For I've always just seen it as a type of poor man's polymorphism, a throw back to the old days, and very rarely use it myself.
Well, there are two sides: With polymorphism it's easy to add new cases. Just write a new class and implement the method. But it's really hard to add new functionality. You have to change every class and add a method. With algebraic data types and pattern matching (which this is a step towards to) it's exactly the other way around. If you want to add a new case you have to change each function matching the cases, but if you want to add new functionality you only have to write exactly one function in one place.
So it's a tradeoff.
SlimerDudeSat 4 Aug 2012
Readable as per the method names. If you trust the code, then you don't need to see the details, just the intent. To re-jig the names in the example:
strSize := StrSize(x)
switch (true) {
case strSize.small:
echo("case1 stuff")
case strSize.big:
echo("case2 stuff")
}
To me, strSize.small reads better than (x.typeof == Str#) && (x as Str).size == 3
I can see where Switch and pattern matching has it's place, I just don't tend to use it much and am surprised that others do. Hmm... maybe I'm missing out... :/
heliumSat 4 Aug 2012
To me, strSize.small reads better than (x.typeof == Str#) && (x as Str).size == 3
If you just want to hide the details of what a "small string" is there is a simpler way: write a method isSmallString.
Jens Tue 31 Jul 2012
This snippet demonstrates how the Fantom switch statement can be used to perform pattern matching logic:
The first branch demonstrates a recursive pattern.
null
acts a the wildcard value here. The matching is performed on the type and value of the fields of the objects.The complete example is stored here: https://github.com/jensli/Misc/blob/master/fan/ExprSimpl.fan
Thought it could be interesting to share.
It works by constructing objects for the patterns, and a special equals method which compares the objects recursively using reflection, ignoring fields that have a wildcard value, or are annotated with the
PatternExclude
facet. The wildcard value can be set with thePatternWildcard
facet.The example is taken from the Programming in Scala book: http://www.artima.com/pins1ed/case-classes-and-pattern-matching.html#15.1
This looks pretty ok in Fantom, the main ugliness is that you can not bind variables of the right type in the branches so you have to cast with
as
.Other disadvantage are that new objects are created each run, and that you need this special equals method.
ivan Tue 31 Jul 2012
Interesting example, thanks!
I also wanted to take more power out of
switch
, but couple of times I found that it is interesting to use pattern like this (just to illustrate an idea):Jens Tue 31 Jul 2012
Neat.
This has the advantage that the objects do not need a special
equals
, but there is more syntactic noise.brian Wed 1 Aug 2012
BTW, I would like to see our switch statement enhanced. Proposals that don't require changes to existing type system are welcome.
ivan Thu 2 Aug 2012
Probably ability to inverse equality order? Like new
switchr
statement, which callscaseN.equals(input)
instead ofinput.equals(caseN)
? This would allow to use case objects which overrideequals
method to perform sort of matching.SlimerDude Thu 2 Aug 2012
Doesn't that break the
equals
contract? Surely:should always be
true
?ivan Thu 2 Aug 2012
It does. Probably more integrate solution is like this:
helium Thu 2 Aug 2012
I agree with SlimerDude. Instead of calling
equals
it should perhaps call a method calledmatches
if it's available and only fall back to the current equals test if not.brian Thu 2 Aug 2012
Having
switch/case
look formatch
instead ofequals
seems elegant. So what all would that buy us?dobesv Fri 3 Aug 2012
Hmm it seems like a halfway solution, though.
Maybe something based on it-blocks would be more sophisticated? An alternative proposal:
it
=the switch parameter and usingit
in scope (as with an it block).The "it-block" style initialization could be handy for unpacking as well:
It's not as compact as you might dream of but it's pretty good. I think it might be better than creating special objects to match against with null checking logic.
SlimerDude Sat 4 Aug 2012
I'm quite surprised by about the amount of enthusiasm there is over the
switch
statement. For I've always just seen it as a type of poor man's polymorphism, a throw back to the old days, and very rarely use it myself.@Dobesv: Your idea of using
it
is quite nice but I wouldn't be keen having new language features introduced for the sake of one keyword e.g. theis
and comma. Plus, as it's in the interests of readability, I get the feeling you're trying to push the language to do what a programmatic solution would do better. As per your example, the following looks far more readable and due to method names, shows more intent also:helium Sat 4 Aug 2012
How is this more readable? The functionality is spread over several classes. To follow the flow I have to jump around between different places.
Well, there are two sides: With polymorphism it's easy to add new cases. Just write a new class and implement the method. But it's really hard to add new functionality. You have to change every class and add a method. With algebraic data types and pattern matching (which this is a step towards to) it's exactly the other way around. If you want to add a new case you have to change each function matching the cases, but if you want to add new functionality you only have to write exactly one function in one place.
So it's a tradeoff.
SlimerDude Sat 4 Aug 2012
Readable as per the method names. If you trust the code, then you don't need to see the details, just the intent. To re-jig the names in the example:
To me,
strSize.small
reads better than(x.typeof == Str#) && (x as Str).size == 3
I can see where
Switch
and pattern matching has it's place, I just don't tend to use it much and am surprised that others do. Hmm... maybe I'm missing out... :/helium Sat 4 Aug 2012
If you just want to hide the details of what a "small string" is there is a simpler way: write a method
isSmallString
.It would be a lot more general, has less overhead and is easier to understand.
Anyway your code doesn't involve any kind of late binding so I'm not sure about what your point is.