Pattern matching structural types in Scala
Asked Answered
C

2

21

Why does this print wtf? Does pattern matching not work on structural types?

  "hello" match {
    case s: { def doesNotExist(i: Int, x: List[_]): Double } => println("wtf?")
    case _ => println("okie dokie")
  }
Cwmbran answered 1/1, 2010 at 8:26 Comment(0)
R
19

Running this example in the Scala interpreter with unchecked warnings on (scala -unchecked) produces the following warning: warning: refinement AnyRef{def doesNotExist(Int,List[_]): Double} in type pattern is unchecked since it is eliminated by erasure. Unfortunately, a generic type like this cannot be checked at runtime as the JVM doesn't have reified generics.

All that the JVM sees in this pattern match is:

"hello" match {
  case s: Object => ... 
  case annon: Object => ...
}

EDIT: In response to your comments, I have been thinking about a solution but didn't have the time to post it yesterday. Unfortunately, even if it should work, the compiler fails to inject the proper Manifest.

The problem you want to solve is to compare if an object is of a given structural type. Here's some code I've been thinking of (Scala 2.8-r20019, as Scala 2.7.6.final crashed on me a couple of times while playing with similar ideas)

type Foo = AnyRef { def doesNotExist(i: Int, x: List[_]): Double }

def getManifest[T](implicit m: Manifest[T]) = m

def isFoo[T](x: T)(implicit mt: Manifest[T]) = 
  mt == getManifest[Foo]

Method isFoo basically compares the manifests of the class x of Foo. In an ideal world, the manifest of a structural type should be equal to the manifest of any type containing the required methods. At least that's my train of thought. Unfortunately this fails to compile, as the compiler injects a Manifest[AnyRef] instead of a Manifest[Foo] when calling getManifest[Foo]. Interestingly enough, if you don't use a structural type (for example, type Foo = String), this code compiles and works as expected. I'll post a question at some point to see why this fails with structural types -- is it a design decision, or it is just a problem of the experimental reflection API.

Failing that, you could always use Java reflection to see if an object contains a method.

def containsMethod(x: AnyRef, name: String, params: java.lang.Class[_]*) = {
  try { 
    x.getClass.getMethod(name, params: _*)
    true
    }
  catch {
    case _ =>  false
  }
}

which works as expected:

containsMethod("foo", "concat", classOf[String]) // true
containsMethod("foo", "bar", classOf[List[Int]]) // false

... but it's not very nice.

Also, note that the structure of a structural type is not available at runtime. If you have a method def foo(x: {def foo: Int}) = x.foo, after erasure you get def foo(x: Object) = [some reflection invoking foo on x], the type information being lost. That's why reflection is used in the first place, as you have to invoke a method on an Object and the JVM doesn't know if the Object has that method.

Rigmarole answered 1/1, 2010 at 11:43 Comment(1)
Thanks, Flaviu. That answers my question. But it still leaves me wondering what the best way to achieve this would be, because the structure is something that actually is available at runtime through reflection. It is just clumsy to get at it.Cwmbran
P
10

If you're going to have to use reflection, you can at least make it look nicer with an extractor:

object WithFoo {
    def foo(){
        println("foo was called")
    }
}

object HasFoo {
    def containsMethod(x: AnyRef, name: String, params: Array[java.lang.Class[_]]) : Boolean = {
        try { 
            x.getClass.getMethod(name, params: _*)
            true
        } catch {
            case _ => false
        }
    }

    def unapply(foo:AnyRef):Option[{def foo():Unit}] = {
        if (containsMethod(foo, "foo", new Array[Class[_]](0))) {
            Some(foo.asInstanceOf[{def foo():Unit}])
        } else None
    }
}


WithFoo.asInstanceOf[AnyRef] match {
    case HasFoo(foo) => foo.foo()
    case _ => println("no foo")
}
Perak answered 8/8, 2010 at 15:15 Comment(3)
Would be nice if it one was able to define HasFoo more flexibly, like val HasFoo = new Has[{def foo():Unit}]("foo"). I just tried to do it that way, but there still seem to be some problems regarding more complicates types such as {def foo(i:Int):Int}.Foozle
Why doesn't the compiler just do this automatically?Roark
containsMethod can be dehydrated to Try(x.getClass.getMethod(name, params: _*)).isSuccessDebauchery

© 2022 - 2024 — McMap. All rights reserved.