Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why did replacing my Scala case class with an extractor break my higher order function?

Tags:

scala

Suppose I have a simple case class that wraps integers, and a higher order method that accepts a function carrying integers to wrappers.

case class Wrapper(x :Int)
def higherOrder(f : Int => Wrapper) = println(f(42))

Then, I can call the higher order function, passing in the wrapper's generated apply function. Amazingly, I can also just pass in the wrapper's name.

higherOrder(Wrapper.apply)  // okay
higherOrder(Wrapper)        // okay, wow!

This is really cool. It allows us to treat the name of the case class as a function, which fosters expressive abstractions. For an example of this coolness, see the answer here. What does "abstract over" mean?

Now suppose my case class isn't powerful enough, and I need to create an extractor instead. As a mildly contrived use case, let's say I need to be able to pattern match on strings that parse into integers.

// Replace Wrapper case class with an extractor
object Wrapper {
    def apply(x :Int) = new Wrapper(x)
    def unapply(s :String) :Option[Wrapper] = {
        // details elided
    }
}
class Wrapper(x :Int) {
    override def toString = "Wrapper(" + x + ")"
    // other methods elided
}

Under this change, I can still pass Wrapper.apply into my higher order function, but passing in just Wrapper no longer works.

higherOrder(Wrapper.apply)  // still okay
higherOrder(Wrapper)        // DOES NOT COMPILE
            ^^^^^^^
//    type mismatch; found : Wrapper.type (with underlying type Wrapper)
//    required: (Int) => Wrapper

Ouch! Here's why this asymmetry is troubling. The advice of Odersky, Spoon, and Venners (Programming in Scala, page 500), says

You could always start with case classes, and then, if the need arises, change to extractors. Because patterns over extractors and patterns over case classes look exactly the same in Scala, pattern matches in your clients will continue to work.

Quite true of course, but we'll break our clients if they are using case class names as functions. And since doing so enables powerful abstractions, some surely will.

So, when passing them into higher order functions, how can we make extractors behave the same as case classes?

like image 819
Morgan Creighton Avatar asked Jan 23 '11 21:01

Morgan Creighton


Video Answer


2 Answers

Simply put, Wrapper (the object) isn't a Function, it's just an object with an apply method. This has absolutely nothing to do with the object also being an extractor.

Try this:

class Wrapper(val x: Int) {
  override def toString = "Wrapper(" + x + ")"
  // other methods elided
}

object Wrapper extends (Int => Wrapper) {
  def apply(x: Int) = new Wrapper(x)
  def unapply(w: Wrapper): Option[Int] = Some(w.x)
}

def higherOrder(f: Int => Wrapper) = println( f(42) )

I also made the parameter to Wrapper a val and swapped around the parameter and return value in your definition of unapply so that it would match case-class behaviour.

like image 90
Kevin Wright Avatar answered Oct 02 '22 15:10

Kevin Wright


Scala extractor companion objects should extend Function

For case classes, the compiler generates a companion object under the hood. This holds relevant static methods for the Wrapper case class, including apply. Peering at the bytecode, we discover that Wrapper's companion object extends Function1. (Actually it extends AbstractFunction1, which uses @specialized to obviate autoboxing.)

This is also noted here. Why do case class companion objects extend FunctionN?

When replacing our Wrapper case class with an extractor, we should take care to extend our home grown companion object with AbstractFunction1 to preserve backwards compatibility for our clients.

This amounts to a small tweak in the source code. The apply method doesn't change at all.

object Wrapper extends scala.runtime.AbstractFunction1[Int, Wrapper] {
like image 39
Morgan Creighton Avatar answered Oct 02 '22 16:10

Morgan Creighton