5

When I have one Option[T] instance it is quite easy to perform any operation on T using monadic operations such as map() and flatMap(). This way I don't have to do checks to see whether it is defined or empty, and chain operations together to ultimately get an Option[R] for the result R.

My difficulty is whether there is a similar elegant way to perform functions on two Option[T] instances.

Lets take a simple example where I have two vals, x and y of type Option[Int]. And I want to get the maximum of them if they are both defined, or the one that is defined if only one is defined, and None if none are defined.

How would one write this elegantly without involving lots of isDefined checks inside the map() of the first Option?

jbx
  • 21,365
  • 18
  • 90
  • 144

8 Answers8

5

You can use something like this:

def optMax(op1:Option[Int], op2: Option[Int]) = op1 ++ op2 match {    
  case Nil => None  
  case list => list.max
}

Or one much better:

def f(vars: Option[Int]*) = (for( vs <- vars) yield vs).max

@jwvh,thanks for a good improvement:

def f(vars: Option[Int]*) = vars.max
3

Usually, you'll want to do something if both values are defined. In that case, you could use a for-comprehension:

val aOpt: Option[Int] = getIntOpt
val bOpt: Option[Int] = getIntOpt

val maxOpt: Option[Int] = 
    for {
        a <- aOpt
        b <- bOpt
    } yield max(a, b)

Now, the problem you described is not as common. You want to do something if both values are defined, but you also want to retrieve the value of an option if only one of them is defined.

I would just use the for-comprehension above, and then chain two calls to orElse to provide alternative values if maxOpt turns out to be None.

maxOpt orElse aOpt orElse bOpt

orElse's signature:

def orElse[B >: A](alternative: ⇒ Option[B]): Option[B]
dcastro
  • 66,540
  • 21
  • 145
  • 155
  • Thanks for your reply. So how would `maxOpt` work if `b` or `a` do not exist? Would it just yield `None`? In my case I have this situation quite common (thats why I wanted to find an elegant solution) because I often have to choose between two items, which possibly are not available both (in which case I take only the available one). – jbx Nov 10 '15 at 17:45
  • @jbx yeah, `maxOpt` would be None if either input is not defined. The compiler actually translates the for comprehension to `aOpt.flatMap { a => bOpt.map { b => max(a, b) } }`. Hope that makes more sense. – dcastro Nov 10 '15 at 17:53
  • Yes it does. In fact I think I prefer it for my purposes, it is more clear to me. So wouldn't this make it complete? `aOpt.flatMap( a => bOpt.map( b => Math.max(a, b))).orElse(bOpt)` – jbx Nov 10 '15 at 18:22
  • @jbx no, that's not enough. If `aOpt = Some(1)` and `bOpt = None`, then your expression could be reduced to `Some(1).flatMap { _ => None } orElse None`, which would be None – dcastro Nov 10 '15 at 18:35
2

Here's another fwiw:

import scala.util.Try
def maxOpt (a:Option[Int]*)= Try(a.flatten.max).toOption

It works with n arguments (including zero arguments).

Mark Lister
  • 1,103
  • 6
  • 16
1

Pattern matching would allow something easy to grasp, but that might not be the most elegant way:

def maxOpt[T](optA: Option[T], optB: Option[T])(implicit f: (T, T) => T): Option[T] = (optA, optB) match {
    case (Some(a), Some(b)) => Some(f(a, b))
    case (None, Some(b)) => Some(b)
    case (Some(a), None) => Some(a)
    case (None, None) => None
}

You end up with something like:

scala> maxOpt(Some(1), None)(Math.max)
res2: Option[Int] = Some(1)

Once you have that building, block, you can use it inside for-comp or monadic operations.

jpmelanson
  • 595
  • 4
  • 10
1

To get maxOpt, you can also use an applicative, which using Scalaz would look like (aOpt |@| bOpt) { max(_, _) } & then chain orElses as @dcastro suggested.

1

I assume you expect Some[Int]|None as a result, not Int|None (otherwise return type has to be Any):

  def maxOption(opts: Option[Int]*) = {
    val flattened = opts.flatten
    flattened.headOption.map { _ => flattened.max }
  }
Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
1

Actually, Scala already gives you this ability more or less directly.

scala> import Ordering.Implicits._
import Ordering.Implicits._

scala> val (a,b,n:Option[Int]) = (Option(4), Option(9), None)
a: Option[Int] = Some(4)
b: Option[Int] = Some(9)
n: Option[Int] = None

scala> a max b
res60: Option[Int] = Some(9)

scala> a max n
res61: Option[Int] = Some(4)

scala> n max b
res62: Option[Int] = Some(9)

scala> n max n
res63: Option[Int] = None
jwvh
  • 50,871
  • 7
  • 38
  • 64
  • Max was just an example. The question was not specifically about max. It is about having any possible function `(T, T) => T` applied converted to `(Option[T], Option[T]) => Option[T]` where if any one of the arguments is `None` the other is returned, and if both are `None`, then `None` is returned. If both are not `None` then some function is applied. – jbx Nov 10 '15 at 21:58
1

A Haskell-ish take on this question is to observe that the following operations:

max, min :: Ord a => a -> a -> a
max a b = if a < b then b else a
min a b = if a < b then a else b

...are associative:

max a (max b c) == max (max a b) c
min a (min b c) == min (min a b) c

As such, any type Ord a => a together with either of these operations is a semigroup, a concept for which reusable abstractions can be built.

And you're dealing with Maybe (Haskell for "option"), which adds a generic "neutral" element to the base a type (you want max Nothing x == x to hold as a law). This takes you into monoids, which are a subtype of semigroups.

The Haskell semigroups library provides a Semigroup type class and two wrapper types, Max and Min, that generically implement the corresponding behaviors.

Since we're dealing with Maybe, in terms of that library the type that captures the semantics you want is Option (Max a)—a monoid that has the same binary operation as the Max semigroup, and uses Nothing as the identity element. So then the function simply becomes:

maxOpt :: Ord a => Option (Max a) -> Option (Max a) -> Option (Max a)
maxOpt a b = a <> b

...which since it's just the <> operator for Option (Max a) is not worth writing. You also gain all the other utility functions and classes that work on Semigroup and Monoid, so for example to find the maximum element of a [Option (Max a)] you'd just use the mconcat function.

The scalaz library comes with a Semigroup and a Monoid trait, as well as Max, Min, MaxVal and MinVal tags that implement those traits, so in fact the stuff that I've demonstrated here in Haskell exists in scalaz as well.

Luis Casillas
  • 29,802
  • 7
  • 49
  • 102