5

I'm trying to combine two Option[Iterable[_]] into a new Option[Iterable[_]]. I would like to return a Some if one (or both) of the elements is a Some and a None otherwise. It seems like there should be an idiomatic way of doing this, but I can't seem to find one. The following seems to do what I want, but isn't quite the slick solution I was hoping for.

def merge(
    i1: Option[Iterable[_]], i2: Option[Iterable[_]]
): Option[Iterable[_]] = (i1, i2) match {
   case (Some(as), Some(bs)) => Some(as ++ bs)
   case (a @ Some(as), None) => a
   case (None, b @ Some(bs)) => b
   case _ => None
}

Any tips are appreciated. Thanks!

dhg
  • 52,383
  • 8
  • 123
  • 144
robo
  • 55
  • 5
  • Kind of almost similar question: http://stackoverflow.com/questions/10617979/binary-operator-with-option-arguments/10618340#10618340, may be helpful – Luigi Plinge Aug 06 '12 at 02:11

3 Answers3

12

If you're willing to put up with a bit of abstract algebra, there's a nice generalization here: Iterable[_] is a monoid under concatenation, where a monoid's just a set of things (iterable collections, in this case) and an addition-like operation (concatenation) with some simple properties and an identity element (the empty collection).

Similarly, if A is a monoid, then Option[A] is also a monoid under a slightly more general version of your merge:

Some(xs) + Some(ys) == Some(xs + ys)
Some(xs) + None     == Some(xs)
None     + Some(ys) == Some(ys)
None     + None     == None

(Note that we need the fact that A is a monoid to know what to do in the first line.)

The Scalaz library captures all these generalizations in its Monoid type class, which lets you write your merge like this:

import scalaz._, Scalaz._

def merge(i1: Option[Iterable[_]], i2: Option[Iterable[_]]) = i1 |+| i2

Which works as expected:

scala> merge(Some(1 to 5), None)
res0: Option[Iterable[_]] = Some(Range(1, 2, 3, 4, 5))

scala> merge(Some(1 to 5), Some(4 :: 3 :: 2 :: 1 :: Nil))
res1: Option[Iterable[_]] = Some(Vector(1, 2, 3, 4, 5, 4, 3, 2, 1))

scala> merge(None, None)
res2: Option[Iterable[_]] = None

(Note that there are other operations that would give valid Monoid instances for Iterable and Option, but yours are the most commonly used, and the ones that Scalaz provides by default.)

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
3

This works:

def merge(i1: Option[Iterable[_]], i2: Option[Iterable[_]]): Option[Iterable[_]] =
  (for (a <- i1; b <- i2) yield a ++ b).orElse(i1).orElse(i2)

The for/yield portion will add the contents of the options if and only if both are Some.

You can also drop some of the dots and parentheses if you want:

  (for (a <- i1; b <- i2) yield a ++ b) orElse i1 orElse i2
dhg
  • 52,383
  • 8
  • 123
  • 144
1

You could use this for arbitrary arity:

def merge(xs: Option[Iterable[_]]*) = 
  if (xs.forall(_.isEmpty)) None else Some(xs.flatten.flatten)
Luigi Plinge
  • 50,650
  • 20
  • 113
  • 180