Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala: map with two or more Options

Basically I'm looking for the most scala-like way to do the following:

def sum(value1: Option[Int], value2: Option[Int]): Option[Int] = 
  if(value1.isDefined && value2.isDefined) Some(value1.get + value2.get)
  else if(value1.isDefined && value2.isEmpty) value1
  else if(value1.isEmpty && value2.isDefined) value2
  else None

This gives correct output:

sum(Some(5), Some(3))  // result = Some(8)
sum(Some(5), None)     // result = Some(5)
sum(None, Some(3))     // result = Some(3)
sum(None, None)        // result = None

Yet to sum more than two options I'd have to use way too many ifs or use some sort of loop.

EDIT-1:

While writing the question I came up with sort of an answer:

def sum2(value1: Option[Int], value2: Option[Int]): Option[Int] = 
  value1.toList ::: value2.toList reduceLeftOption { _ + _ }

This one looks very idiomatic to my inexperienced eye. This would even work with more than two values. Yet is possible to do the same without converting to lists?

EDIT-2:

I ended up with this solution (thanks to ziggystar):

def sum(values: Option[Int]*): Option[Int] = 
  values.flatten reduceLeftOption { _ + _ }

EDIT-3:

Another alternative thanks to Landei:

def sum(values: Option[Int]*): Option[Int] = 
  values collect { case Some(n) => n } reduceLeftOption { _ + _ }
like image 593
Vilius Normantas Avatar asked Apr 29 '11 13:04

Vilius Normantas


5 Answers

You can make it very concise using the fact that there is an Semigroup instance for Option that does exactly what you want. You can use scalaz or cats. Here is an example using cats:

import cats.std.option._
import cats.syntax.semigroup._
import cats.std.int._

Option(1) |+| Option(2) // Some(3)
Option(1) |+| None      // Some(1)
None      |+| Option(2) // Some(2)

So your sum becomes:

def sum(v1: Option[Int], v2: Option[Int]): Option[Int] = v1 |+| v2
like image 108
Markus1189 Avatar answered Oct 06 '22 01:10

Markus1189


Reduced solution of michael.kebe with a little look to some basic mathematical rules:

def sum(a: Option[Int], b: Option[Int]) = (a,b) match {
  case (None,None) => None
  case _ => Some(a.getOrElse(0)+b.getOrElse(0))
}

scala> sum(Some(5), Some(3))  // result = Some(8)
res6: Option[Int] = Some(8)

scala> sum(Some(5), None)     // result = Some(5)
res7: Option[Int] = Some(5)

scala> sum(None, Some(3))     // result = Some(3)
res8: Option[Int] = Some(3)

scala> sum(None, None)        // result = None
res9: Option[Int] = None
like image 33
Antonin Brettsnajdr Avatar answered Oct 06 '22 00:10

Antonin Brettsnajdr


How about:

scala> def sum(values: Option[Int]*): Option[Int] = values.flatten match {
     | case Nil => None                                                   
     | case l => Some(l.sum)                                              
     | }
sum: (values: Option[Int]*)Option[Int]

scala> sum(Some(1), None)
res0: Option[Int] = Some(1)

scala> sum(Some(1), Some(4))
res1: Option[Int] = Some(5)

scala> sum(Some(1), Some(4), Some(-5))
res3: Option[Int] = Some(0)

scala> sum(None, None)                
res4: Option[Int] = None

Edit

Maybe it would be sane to return 0 if all arguments were None. In that case the function would reduce to values.flatten.sum.

like image 28
ziggystar Avatar answered Oct 06 '22 00:10

ziggystar


scala> def sum(a: Option[Int], b: Option[Int]) = (a,b) match {
     |   case (Some(x), Some(y)) => Some(x + y)
     |   case (Some(x), None) => Some(x)
     |   case (None, Some(y)) => Some(y)
     |   case _ => None
     | }
sum: (a: Option[Int],b: Option[Int])Option[Int]

scala> sum(Some(5), Some(3))
res0: Option[Int] = Some(8)

scala> sum(Some(5), None)
res1: Option[Int] = Some(5)

scala> sum(None, Some(3))
res2: Option[Int] = Some(3)

scala> sum(None, None)
res3: Option[Int] = None
like image 33
michael.kebe Avatar answered Oct 06 '22 01:10

michael.kebe


Another solution is:

def sum(values: Option[Int]*): Int = values.collect{case Some(n) => n}.sum

While in the current case flatten is clearly more convenient, the collect version is more flexible, as it allows to perform mappings and to have additional filter conditions or complex patterns. E.g. imagine you want to have the sum of the squares of all even numbers in values:

values.collect{case Some(n) if n mod 2 == 0 => n*n}.sum
like image 38
Landei Avatar answered Oct 06 '22 00:10

Landei