1

I have recently faced a confusing issue in Scala. I expect the following code to result in None, but it results in Some(null):

Option("a").map(_ => null)

What is the reasoning behind this? Why does it not result in None?

Note: This question is not a duplicate of Why Some(null) isn't considered None?, as that questions asks for explicitly using Some(null). My question is about using Option.map.

Community
  • 1
  • 1
Hakan Serce
  • 11,198
  • 3
  • 29
  • 48
  • Possible duplicate of [Why Some(null) isn't considered None?](http://stackoverflow.com/questions/5796616/why-somenull-isnt-considered-none) – awesoon Apr 10 '16 at 06:51
  • @soon, this isn't a duplicate of that question. That question explicitly asks for Some(null). I already checked that. Thanks. – Hakan Serce Apr 10 '16 at 07:04
  • 2
    Yes, not an exactly duplicate, but it covers `Some(null)` behavior which is closely connected to your question as it used under the hood of the `map` method. I've retracted my vote anyway – awesoon Apr 10 '16 at 07:21
  • Instance of `Some` is not `None`, as naming suggests. That's why `Some(null)` is _something_ holding `null`, not 'nothing'. – Victor Sorokin Apr 10 '16 at 07:25

5 Answers5

9

Every time we add an exception to a rule, we deprive ourselves of a tool for reasoning about code.

Mapping over a Some always evaluates to a Some. That's a simple and useful law. If we were to make the change you propose, we would no longer have that law. For example, here's a thing we can say with certainty. For all f, x, and y:

Some(x).map(f).map(_ => y) == Some(y)

If we were to make the change you propose, that statement would no longer be true; specifically, it would not hold for cases where f(x) == null.

Moreover, Option is a functor. Functor is a useful generalization of things that have map functions, and it has laws that correspond well to intuition about how mapping should work. If we were to make the change you propose, Option would no longer be a functor.

null is an aberration in Scala that exists solely for interoperability with Java libraries. It is not a good reason to discard Option's validity as functor.

Chris Martin
  • 30,334
  • 10
  • 78
  • 137
  • 1
    Btw, as interesting side note Java's Optional doesn't follow Functor's laws: https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#map-java.util.function.Function- – pedrofurla Apr 10 '16 at 18:30
2

Here is the code for Option map method:

/** Returns a $some containing the result of applying $f to this $option's
 * value if this $option is nonempty.
 * Otherwise return $none.
 *
 *  @note This is similar to `flatMap` except here,
 *  $f does not need to wrap its result in an $option.
 *
 *  @param  f   the function to apply
 *  @see flatMap
 *  @see foreach
 */
@inline final def map[B](f: A => B): Option[B] =
  if (isEmpty) None else Some(f(this.get))

So, as you can see, if the option is not empty, it will map to Some with the value returned by the function. And here is the code for Some class:

/** Class `Some[A]` represents existing values of type
 *  `A`.
 *
 *  @author  Martin Odersky
 *  @version 1.0, 16/07/2003
 */
@SerialVersionUID(1234815782226070388L) // value computed by serialver for 2.11.2, annotation added in 2.11.4
final case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
  def get = x
}

So, as you can see, Some(null) will actually create a Some object containing null. What you probably want to do is use Option.apply which does returns a None if the value is null. Here is the code for Option.apply method:

/** An Option factory which creates Some(x) if the argument is not null,
 *  and None if it is null.
 *
 *  @param  x the value
 *  @return   Some(value) if value != null, None if value == null
 */
def apply[A](x: A): Option[A] = if (x == null) None else Some(x)

So, you need to write your code like this:

Option("a").flatMap(s => Option.apply(null))

Of course, this code makes no sense, but I will consider that you are just doing some kind of experiment.

marcospereira
  • 12,045
  • 3
  • 46
  • 52
  • Thanks, considering the source code you provided for `Option.map` and `Some`, I can understand why it results this way. I'll accept your answer. Apart from that, though, isn't it confusing this way? Wouldn't it make more sense if it just returned `None` in this case? – Hakan Serce Apr 10 '16 at 07:16
  • Probably, not `Option("a").map(s => Option.apply(null))`, but `Option("a").flatMap(s => Option.apply(null))` (or even `Option("a").flatMap(s => Option(null))`)? – awesoon Apr 10 '16 at 07:22
  • Your last statement evaluates to `Some(None)`. which is of type `Option[Option[Nothing]]`. You should use `flatmap` instead as other questions pointed out. – maasg Apr 10 '16 at 10:44
  • Corrected. Thanks @maasg. – marcospereira Apr 10 '16 at 15:36
2

Option is kind of replacement for null, but in general you see null in scala when you are talking to some java code, it is not like Option is supposed to handle nulls whenever possible, it is not designed to be used with nulls but instead of them. There is however conveniece method Option.apply that is similar to java's Optional.ofNullable that would handle the null case, and that's mostly all about nulls and Options in scala. In all other cases it works on Some and None not making any difference if null is inside or not.

If you have some nasty method returning null that comes from java and you want to use it directly, use following approach:

def nastyMethod(s: String): String = null

Some("a").flatMap(s => Option(nastyMethod(s)))
// or
Some("a").map(nastyMethod).flatMap(Option(_))

Both output Option[String] = None

So, nastyMethod can return a String or null conceptually is an Option, so wrap its result in an Option and use it as an Option. Don't expect null magic will happen whenever you need it.

Łukasz
  • 8,555
  • 2
  • 28
  • 51
2

To understand what's going on, we can use the functional substitution principle to explore the given expression step by step:

Option("a").map(s => null) // through Option.apply
Some("a").map(s => null) // let's name the anonymous function as: f(x) = null 
Some("a").map(x => f(x))  // following Option[A].map(f:A=>B) => Option[B]
Some(f("a"))  // apply f(x)
Some(null)

The confusion expressed in the question comes from the assumption that the map would apply to the argument of the Option before the Option.apply is evaluated: Let's see how that couldn't possibly work:

Option("a").map(x=> f(x)) // !!! can't evaluate map before Option.apply. This is the key to understand !
Option(f(a))              // !!! we can't get here
Option(null)              // !!! we can't get here
None                      // !!! we can't get here
maasg
  • 37,100
  • 11
  • 88
  • 115
0

Why would it be None, the signature of map is a function from a value A to B to yield an Option[B]. No where in that signature does it indicate that B may be null by saying B is an Option[B]. flatMap however does indicate that the values returned is also optional. It's signature is Option[A] => (A => Option[B]) => Option[B].

Milyardo
  • 1
  • 1
  • So in this case I would expect it to return `Option(null)`, which in turn would give `None`. That is exactly the reason for my confusion. – Hakan Serce Apr 10 '16 at 07:07