Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combining Scala Option[Iterable[_]]

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!

like image 698
robo Avatar asked Aug 05 '12 22:08

robo


3 Answers

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.)

like image 197
Travis Brown Avatar answered Oct 05 '22 15:10

Travis Brown


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
like image 30
dhg Avatar answered Oct 05 '22 13:10

dhg


You could use this for arbitrary arity:

def merge(xs: Option[Iterable[_]]*) = 
  if (xs.forall(_.isEmpty)) None else Some(xs.flatten.flatten)
like image 39
Luigi Plinge Avatar answered Oct 05 '22 13:10

Luigi Plinge