#1982 Poor man's pattern matching

Jens Tue 31 Jul 2012

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.

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 the PatternWildcard 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):

class Main
{
  static |Obj -> Obj?| matcher(|Obj, Obj->Bool| match, Obj[][] cases) 
  {
    |Obj input -> Obj?| 
    {
      cases.find { match(it.first, input) }?.last?->call
    }
  }

  static Void main(Str[] args)
  {  
    match := Obj#equals.func
    matcher := matcher(match,
      [  //poor man's ordered map literal
        [3, |->| { echo("this is three") } ],
        [42, |->| { echo("this is 42")} ]
      ]
    )

    matcher(3) //prints 'this is three'
  }

}

Jens Tue 31 Jul 2012

it is interesting to use pattern like this

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 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.

SlimerDude Thu 2 Aug 2012

Doesn't that break the equals contract? Surely:

caseN.equals(input) == input.equals(caseN)

should always be true?

ivan Thu 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;
}

helium Thu 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.

brian Thu 2 Aug 2012

Having switch/case look for match instead of equals 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:

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.

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. 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
  }
}

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.

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.

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:

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... :/

helium Sat 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.

class Wotever {
  static Void main(Str[] args) {
    x := "wot"  // "wotever"

    switch (true) {
      case StrSize.isSmallString(x):
        echo("case1 stuff")

      case StrSize.isBigString(x):
        echo("case2 stuff")
    }
  }
}

class StrSize {
  static Bool isSmallString(Str s)
  {
     return s.size == 3;
  }

  static Bool isBigString(Str s)
  {
     return s.size > 3;
  }
}

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.

Login or Signup to reply.