Type Error when using None as a default to Option.fold


Type Erasure, Option, and Folding

The Tl;dr

If you get error: type mismatch; while specifying None as the default in a .fold on an Option. Use a closure in your default case and specify the type like so: option.fold { val thing = Option[MyType] = None; thing } { ... }

Long Story:

Everyone likes type inference. It saves a lot of writing on a programmers part when instead of writing a behemoth of some kind like this:

val i : Future[Option[my.package.model.ObjectThing]] = someMethod

and can instead just write:

val i = someMethod

And the compiler keeps track of your type for you. It's handy, and something Haskell, OCaml, and Scala (among others) enjoy. However, type inference isn't always as easy as reading a method's return type. Take this example:

val s : Option[Int] = someMethod()
val r = s.fold( 0 ) { x =>
    x +  1

What's the type of r in this case? Easy! It's an Integer! I only recently discovered the idiom of folding on options, so let me describe what's going on here. First, fold is often seen as a way to implement a reducing operation on a list:

val myList = List(1,2,3)
val q = myList.fold(0) { (l, r) => l + r } 
// q is 6

q is 6 in this case because we start at 0, and then take our current left value (which is 0 at first) and then add it to the next element in our list, 1. On the next iteration the l will be the result of our previous computation (1), and we'll add 2. The result being 3, we'll then have 3 as our l value and the last element in the list (3) as r. Lastly, we'll do our add again and q will be the result of 3 + 3.

So what's up with that fold on the Option class then? Well, besides that the community in scala in split between sticking to getOrElse and using fold on option, it's a handy and type safe way of dealing with options. A complaint levied against using fold is that the order isn't the same as getOrElse:

val o = Option[Int] = None
o.getOrElse(2) // yields 2 
o.fold(2)( x => x) // yields 2

Having the default first seems to throw people off. But here's why I think it makes sense. In the case of our reduce example, we seeded the left hand side of our function with the first set of parameters. In the case of folding our option, we seeded our result, if it didn't have a value already. When I think of it this way, it's easy to get into the habit of staying consistent with our folds.

Moving back to our point about type inference, can you tell me what the type of s is?

val o = Option[Int] = None
val s = o.getOrElse("hi")

If you said: String congratulations! You're wrong. The correct type is Any. Why? Because o would give us an Int if it had a value, and String if it didn't, but since we don't know (assume o came from someMethod) until runtime, the compiler has to default to the common base type, which in the case of Int and String is Any. But Ethan, you say, you said fold is safer? Yup. Let's try the same with fold:

val o = Option[Int] = None
val s = o.fold("hi") { x => x }

<console>:8: error: type mismatch;
found   : Int
required: String
     val s = o.fold("hi") { x => x }

So now instead of having to deal with Any and losing our type, we get a compiler level error that tells us that we might want to be more careful and return the same type so we can reason about it easier. In the case of Option.fold, the default will determine which type the compiler requires.

Now to the reason I'm writing this blog post: What do you about type erasure when dealing with a class wrapped in Option? To make what I'm asking clear, let's say you have this:

val o = Option[Int] = ??? //pick Some(1) or None
o.fold(None) { x => Some(x) }

The above will not compile. In fact, you'll get the following error message:

error: type mismatch;
  found   : Some[Int]
  required: None.type

Confused? After all, you can assign none to an Option[Int], so why is the compiler so strict and how do you get around it? You might think specificying the result of o.fold would help. Nope. Here's a hack around it:

val o = Option[Int] = ???
o.fold { 
    val tmp : Option[Int] = None
} { x => Some(x) } 

By doing the above, we let the compiler figure out that yes, the two types in both closures of the fold match. And they match because we now have a common ancestor type:

scala> val o : Option[Int] = None
scala> o.getClass()
scala> res6: Class[_ <: Option[Int]] = class scala.None$

scala> val o : Option[Int] = Some(1)
scala> o.getClass()
res7: Class[_ <: Option[Int]] = class scala.Some

Notice the Class[_ <: Option[Int]]? That's how you can tell that they both are descendents of Option[Int]. If you check the class of None:

scala> val x =None    
scala> x.getClass()
res8: Class[_ <: None.type] = class scala.None$

you'll see what should be obvious by now, that they don't share the same common base.

I hope this helps anyone else who runs into type mismatch errors from the None.type while folding Option instances!

Other Posts

comments powered by Disqus