2
votes

I have the following data model which I'm going to do pattern matching against later:

abstract class A
case class C(s:String) extends A

abstract class B extends A
case class D(i:Int) extends B
case class E(s:Int, e:Int) extends B

A is the abstract super type of the hierarchy. C is a concrete subclass of A. Other concrete subclasses of A are subclasses of B which is in turn a subclass of A.

Now if I write something like this, it works:

def match(a:A) a match {
   a:C => println("C")
   a:B => println("B")
}

However, in a for loop I cannot match against B. I assume that I need a constructor pattern, but since B is abstract, there is no constructor pattern for B.

val list:List[A] = List(C("a"), D(1), E(2,5), ...)

for (b:B <- list) println(b)  // Compile error
for (b@B <- list) println(b)  // Compile error

Here, I would like to print only B instances. Any workaround for this case?

4

4 Answers

3
votes

You can use collect:

list.collect { case b: B => println(b) }

If you want to better undertand this, I recommend to read about partial functions. Here for example.

2
votes

Sergey is right; you'll have to give up for if you want to pattern match and filter only B instances. If you still want to use a for comprehension for whatever reason, I think one way is to just resort to using a guard:

for (b <- list if b.isInstanceOf[B]) println(b)

But it's always best to pick pattern-matching instead of isInstanceOf. So I'd go with the collect suggestion (if it made sense in the context of the rest of my code).

Another suggestion would be to define a companion object to B with the same name, and define the unapply method:

abstract class A
case class C(s:String) extends A

abstract class B extends A
object B { def unapply(b: B) = Option(b)  } // Added a companion to B
case class D(i:Int) extends B
case class E(s:Int, e:Int) extends B

Then you can do this:

for (B(b) <- list) println(b)

So that's not the 'constructor' of B, but the companion's unapply method. It works, and that's what friends are for, right?

(See http://www.scala-lang.org/node/112 )

2
votes

If you ask me, the fact that you can't use pattern matching here is an unfortunate inconsistency of scala. Indeed scala does let you pattern match in for comprehensions, as this example will show:

val list:List[A] = List(C("a"), D(1), E(2,5)
for ((b:B,_) <- list.map(_ -> null)) println(b)

Here I temporarily wrap the elements into pairs (whith a dummy and unused second value) and then pattern match for a pair where the first element is of type B. As the output shows, you get the expected behaviour:

 D(1)
 E(2,5)

So there you go, scala does support filtering based on pattern matching (even when matching by type), it just seems that the grammar does not handle pattern matching a single element by type.

Obviously I am not advising to use this trick, this was just to illustrate. Using collect is certainly better.

Then again, there is another, more general solution if for some reason you really fancy for comprehensions more than anything:

object Value {
  def unapply[T]( value: T ) = Some( value )
}
for ( Value(b:B) <- list ) println(b)

We just introduced a dummy extractor in the Value object which just does nothing, so that Value(b:B) has the same meaning as just b:B, except that the former does compile. And unlike my earlier trick with pairs, it is relatively readable, and Value only has to be written once, you can use it at will then (in particular, no need for writing a new extractor for each type you want to pattern match against, as in @Faiz's answer. I'll let you find a better name than Value though.

Finally, there is another work around that works out of the box (credit goes to Daniel Sobral), but is slightly less readable and requires a dummy identifier (here foo):

for ( b @(foo:B) <- list ) println(b)
// or similarly:
for ( foo @(b:B) <- list ) println(b)
0
votes

my 2 cents: You can add a condition in the for comprehension checking type but that would NOT be as elegant as using collect which would take only instances of class B.