Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

generic type-based function to sum numbers based on their types

Tags:

scala

Suppose x and y are of the same type and can be either Boolean, Int, or Double. Here is the function I want to write:

f(x, y) = 
   - if x == Boolean ==>   !x 
   - if x == Integer or x == Double ==> x+ y 

One way of doing this can be the following. I was wondering if anyone has a better ideas on this.

def fun[T](x: T, y: T): T { 
   x match { 
       case xP: Boolean => !xP 
       case xP: Double => y match { case yP: Double =>  xP + yP }
       case xP: Int => y match { case yP: Int =>  xP + yP }
   }
}

The reason I am not happy with this is that x and y have the same type. I shouldn't need two match-cases; right?

Two other things:

  • Is it enough to just set [T <: Int, Double, Boolean] in order to restrict the type to only three types?
  • The output type needs to be again T.
like image 882
Daniel Avatar asked Feb 13 '16 19:02

Daniel


2 Answers

This is precisely the kind of problem that type classes are designed to solve. In your case you could write something like this:

trait Add[A] {
  def apply(a: A, b: A): A
}

object Add {
  implicit val booleanAdd: Add[Boolean] = new Add[Boolean] {
    def apply(a: Boolean, b: Boolean): Boolean = !a
  }

  implicit def numericAdd[A: Numeric]: Add[A] = new Add[A] {
    def apply(a: A, b: A): A = implicitly[Numeric[A]].plus(a, b)
  }
}

A value of type Add[X] describes how to add two values of type X. You put implicit "instances" of type Add[X] in scope for every type X that you want to be able to perform this operation on. In this case I've provided instances for Boolean and any type that has an instance of scala.math.Numeric (a type class that's provided by the standard library). If you only wanted instances for Int and Double, you could simply leave out numericAdd and write your own Add[Int] and Add[Double] instances.

You'd write your fun like this:

def fun[T: Add](x: T, y: T) = implicitly[Add[T]].apply(x, y)

And use it like this:

scala> fun(true, false)
res0: Boolean = false

scala> fun(1, 2)
res1: Int = 3

scala> fun(0.01, 1.01)
res2: Double = 1.02

This has the very significant advantage of not blowing up at runtime on types that you haven't defined the operation for. Instead of crashing your program with a MatchError exception when you pass e.g. two strings to fun, you get a nice compilation failure:

scala> fun("a", "b")
<console>:14: error: could not find implicit value for evidence parameter of type Add[String]
       fun("a", "b")
          ^

In general "type case" matching (i.e. matches that look like case x: X => ...) are a bad idea in Scala, and there's almost always a better solution. Often it'll involve type classes.

like image 96
Travis Brown Avatar answered Sep 22 '22 10:09

Travis Brown


If you want a generic function for summing numbers, you can make a trait Summable[A] with implicit conversions from the numbers you want to Summable. These conversions can be implicit methods or they can be methods in implicit objects, latter being shown below.

trait Summable[A] {
  def +(a: A, b: A): A
}

object Summable {
  implicit object SummableBoolean extends Summable[Boolean] {
    override def +(a: Boolean, b: Boolean) = !a
  }
  implicit object SummableInt extends Summable[Int] {
    override def +(a: Int, b: Int) = a + b
  }
  implicit object SummableDouble extends Summable[Double] {
    override def +(a: Double, b: Double) = a + b
  }
}

def fun[A](a: A, b: A)(implicit ev: Summable[A]) =
  ev.+(a, b)

val res1 = fun(true, true) // returns false
val res2 = fun(1, 3) // returns 4
val res3 = fun(1.5, 4.3) // returns "5.8"

This is called a type class pattern. I included the boolean case because you asked for it, but I strongly believe that it has no place in a function which sums elements. One nice rule to follow is to have each function do one thing and one thing only. Then you can easily compose them into bigger functions. Inverting boolean has no place in a function that sums its arguments.

like image 44
slouc Avatar answered Sep 18 '22 10:09

slouc