Search code examples
scalaoption-type

Is there a concise way to "invert" an Option?


Say I have a function that can take an optional parameter, and I want to return a Some if the argument is None and a None if the argument is Some:

def foo(a: Option[A]): Option[B] = a match {
  case Some(_) => None
  case None    => Some(makeB())
}

So what I want to do is kind of the inverse of map. The variants of orElse are not applicable, because they retain the value of a if it's present.

Is there a more concise way to do this than if (a.isDefined) None else Some(makeB())?


Solution

  • Overview of this answer:

    1. One-liner solution using fold
    2. Little demo with the fold
    3. Discussion of why the fold-solution could be just as "obvious" as the if-else-solution.

    Solution

    You can always use fold to transform Option[A] into whatever you want:

    a.fold(Option(makeB())){_ => Option.empty[B]}
    

    Demo

    Here is a complete runnable example with all the necessary type definitions:

    class A
    class B
    def makeB(): B = new B
    
    def foo(a: Option[A]): Option[B] = a match {
      case Some(_) => None
      case None    => Some(makeB())
    }
    
    def foo2(a: Option[A]): Option[B] = 
      a.fold(Option(makeB())){_ => Option.empty[B]}
    
    println(foo(Some(new A)))
    println(foo(None))
    println(foo2(Some(new A)))
    println(foo2(None))
    

    This outputs:

    None
    Some(Main$$anon$1$B@5fdef03a)
    None
    Some(Main$$anon$1$B@48cf768c)
    

    Why fold only seems less intuitive

    In the comments, @TheArchetypalPaul has commented that fold seems "lot less obvious" than the if-else solution. I agree, but I still think that it might be interesting to reflect on the reasons why that is.

    I think that this is mostly an artifact resulting from the presence of special if-else syntax for booleans.

    If there were something like a standard

    def ifNone[A, B](opt: Option[A])(e: => B) = new {
      def otherwise[C >: B](f: A => C): C = opt.fold((e: C))(f)
    }
    

    syntax that can be used like this:

    val optStr: Option[String] = Some("hello")
    
    val reversed = ifNone(optStr) { 
      Some("makeB") 
    } otherwise {
      str => None
    }
    

    and, more importantly, if this syntax was mentioned on the first page of every introduction to every programming language invented in the past half-century, then the ifNone-otherwise solution (that is, fold), would look much more natural to most people.

    Indeed, the Option.fold method is the eliminator of the Option[T] type: whenever we have an Option[T] and want to get an A out of it, the most obvious thing to expect should be a fold(a)(b) with a: A and b: T => A. In contrast to the special treatment of booleans with the if-else-syntax (which is a mere convention), the fold method is very fundamental, the fact that it must be there can be derived from the first principles.