Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Extractor with Argument

Is there a syntax in Scala that allows extractors to take a customization argument? This example is a bit contrived. Suppose I have a binary search tree on integers, and I want to match on the current node if its value is divisible by some custom value.

Using F# active patterns, I can do the following:

type Tree =
    | Node of int * Tree * Tree
    | Empty  

let (|NodeDivisibleBy|_|) x t =
    match t with
    | Empty -> None
    | Node(y, l, r) -> if y % x = 0 then Some((l, r)) else None

let doit = function
    | NodeDivisibleBy(2)(l, r) -> printfn "Matched two: %A %A" l r
    | NodeDivisibleBy(3)(l, r) -> printfn "Matched three: %A %A" l r
    | _ -> printfn "Nada"

[<EntryPoint>]
let main args =
    let t10 = Node(10, Node(1, Empty, Empty), Empty)
    let t15 = Node(15, Node(1, Empty, Empty), Empty)

    doit t10
    doit t15

    0

In Scala, I can do something similar, but not quite what I want:

sealed trait Tree
case object Empty extends Tree
case class Node(v: Int, l: Tree, r: Tree) extends Tree

object NodeDivisibleBy {
  def apply(x: Int) = new {
    def unapply(t: Tree) = t match { 
      case Empty => None
      case Node(y, l, r) => if (y % x == 0) Some((l, r)) else None
    }
  }
}

def doit(t: Tree) {
  // I would prefer to not need these two lines.
  val NodeDivisibleBy2 = NodeDivisibleBy(2)
  val NodeDivisibleBy3 = NodeDivisibleBy(3)
  t match { 
    case NodeDivisibleBy2(l, r) => println("Matched two: " + l + " " + r)
    case NodeDivisibleBy3(l, r) => println("Matched three: " + l + " " + r)
    case _ => println("Nada")
  }
}

val t10 = Node(10, Node(1, Empty, Empty), Empty)
val t15 = Node(15, Node(1, Empty, Empty), Empty)

doit(t10)
doit(t15)

It would be great if I could do:

case NodeDivisibleBy(2)(l, r) => println("Matched two: " + l + " " + r)
case NodeDivisibleBy(3)(l, r) => println("Matched three: " + l + " " + r)

but this is a compile time error: '=>' expected but '(' found.

Thoughts?

like image 806
Fysx Avatar asked Nov 05 '12 23:11

Fysx


3 Answers

From the spec:

   SimplePattern ::= StableId ‘(’ [Patterns] ‘)’

An extractor pattern x(p1, ..., pn) where n ≥ 0 is of the same syntactic form as a constructor pattern. However, instead of a case class, the stable identifier x denotes an object which has a member method named unapply or unapplySeq that matches the pattern.

and:

A stable identifier is a path which ends in an identifier.

i.e., not an expression like NodeDivisibleBy(2).

So no, this isn't possible in any straightforward way in Scala, and personally I think that's just fine: having to write the following (which by the way I'd probably define in the NodeDivisibleBy object and import):

val NodeDivisibleBy2 = NodeDivisibleBy(2)
val NodeDivisibleBy3 = NodeDivisibleBy(3)

is a small price to pay for the increased readability of not having to decipher arbitrary expressions in the case clause.

like image 50
Travis Brown Avatar answered Nov 13 '22 21:11

Travis Brown


As Travis Brown has noted, it's not really possible in scala.

What I do in that scenario is just separating the decomposition from the test with a guard and an alias.

val DivisibleBy = (n: Node, x: Int) => (n.v % x == 0) 

def doit(t: Tree) = t match { 
    case n @ Node(y, l, r) if DivisibleBy(n,2) => println("Matched two: " + l + " " + r)
    case n @ Node(y, l, r) if DivisibleBy(n,3) => println("Matched three: " + l + " " + r)
    case _ => println("Nada")
}

Defining a separate DivisibleBy is obviously complete overkill in this simple case, but may help readability in more complex scenarios in a similar way as F# active patterns do.

You could also define divisibleBy as a method of Node and have:

case class Node(v: Int, l: Tree, r: Tree) extends Tree {
    def divisibleBy(o:Int) = (v % o)==0
}

def doit(t: Tree) = t match { 
    case n @ Node(y, l, r) if n divisibleBy 2 => println("Matched two: " + l + " " + r)
    case n @ Node(y, l, r) if n divisibleBy 3 => println("Matched three: " + l + " " + r)
    case _ => println("Nada")
}

which I think is more readable (if more verbose) than the F# version

like image 43
Paolo Falabella Avatar answered Nov 13 '22 22:11

Paolo Falabella


Bind together the case class, predicate and arguments, then match on the result as usual.

case class Foo(i: Int)

class Testable(val f: Foo, val ds: List[Int])

object Testable {
  def apply(f: Foo, ds: List[Int]) = new Testable(f, ds)
  def unapply(t: Testable): Option[(Foo, List[Int])] = {
    val xs = t.ds filter (t.f.i % _ == 0)
    if (xs.nonEmpty) Some((t.f, xs)) else None
  }
}

object Test extends App {
  val f = Foo(100)

  Testable(f, List(3,5,20)) match {
    case Testable(f, 3 :: Nil)  => println(s"$f matched three")
    case Testable(Foo(i), 5 :: Nil) if i < 50
                                => println(s"$f matched five")
    case Testable(f, ds)        => println(s"$f matched ${ds mkString ","}")
    case _                      => println("Nothing")
  }
}
like image 2
som-snytt Avatar answered Nov 13 '22 20:11

som-snytt