53
votes

I am trying to learn Scala now, with a little bit of experience in Haskell. One thing that stood out as odd to me is that all function parameters in Scala must be annotated with a type - something that Haskell does not require. Why is this? To try to put it as a more concrete example: an add function is written like this:

def add(x:Double, y:Double) = x + y

But, this only works for doubles(well, ints work too because of the implicit type conversion). But what if you want to define your own type that defines its own + operator. How would you write an add function which works for any type that defines a + operator?

5
Glad I saw this before posting my own almost carbon copy. Thanks for asking.wheaties

5 Answers

70
votes

Haskell uses Hindley-Milner type inference algorithm whereas Scala, in order to support Object Oriented side of things, had to forgo using it for now.

In order to write an add function for all applicable types easily, you will need to use Scala 2.8.0:

Welcome to Scala version 2.8.0.r18189-b20090702020221 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_15).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import Numeric._
import Numeric._

scala> def add[A](x: A, y: A)(implicit numeric: Numeric[A]): A = 
     | numeric.plus(x, y)
add: [A](x: A,y: A)(implicit numeric: Numeric[A])A

scala> add(1, 2)
res0: Int = 3

scala> add(1.1, 2.2)
res1: Double = 3.3000000000000003
18
votes

In order to solidify the concept of using implicit for myself, I wrote an example that does not require scala 2.8, but uses the same concept. I thought it might be helpful for some. First, you define an generic-abstract class Addable:

scala> abstract class Addable[T]{
 |   def +(x: T, y: T): T
 | }
defined class Addable

Now you can write the add function like this:

scala> def add[T](x: T, y: T)(implicit addy: Addable[T]): T = 
 | addy.+(x, y)
add: [T](T,T)(implicit Addable[T])T

This is used like a type class in Haskell. Then to realize this generic class for a specific type, you would write(examples here for Int, Double and String):

scala> implicit object IntAddable extends Addable[Int]{
 |   def +(x: Int, y: Int): Int = x + y
 | }
defined module IntAddable

scala> implicit object DoubleAddable extends Addable[Double]{
 |   def +(x: Double, y: Double): Double = x + y
 | }
defined module DoubleAddable

scala> implicit object StringAddable extends Addable[String]{
 |   def +(x: String, y: String): String = x concat y
 | }
defined module StringAddable

At this point you can call the add function with all three types:

scala> add(1,2)
res0: Int = 3

scala> add(1.0, 2.0)
res1: Double = 3.0

scala> add("abc", "def")
res2: java.lang.String = abcdef

Certainly not as nice as Haskell which will essentially do all of this for you. But, that's where the trade-off lies.

3
votes

Haskell uses the Hindley-Milner type inference. This kind of type-inference is powerful, but limits the type system of the language. Supposedly, for instance, subclassing doesn't work well with H-M.

At any rate, Scala type system is too powerful for H-M, so a more limited kind of type inference must be used.

3
votes

I think the reason Scala requires the type annotation on the parameters of a newly defined function comes from the fact that Scala uses a more local type inference analysis than that used in Haskell.

If all your classes mixed in a trait, say Addable[T], that declared the + operator, you could write your generic add function as:

def add[T <: Addable[T]](x : T, y : T) = x + y

This restricts the add function to types T that implement the Addable trait.

Unfortunately, there is not such trait in the current Scala libraries. But you can see how it would be done by looking at a similar case, the Ordered[T] trait. This trait declares comparison operators and is mixed in by the RichInt, RichFloat, etc. classes. Then you can write a sort function that can take, for example, a List[T] where [T <: Ordered[T]] to sort a list of elements that mix in the ordered trait. Because of implicit type conversions like Float to RichFloat, you can even use your sort function on lists of Int, or Float or Double.

As I said, unfortunately, there is no corresponding trait for the + operator. So, you would have to write out everything yourself. You would do the Addable[T] trait, create AddableInt, AddableFloat, etc., classes that extend Int, Float, etc. and mix in the Addable trait, and finally add implicit conversion functions to turn, for example, and Int into an AddableInt, so that the compiler can instantiate and use your add function with it.

1
votes

The function itself will be pretty straightforward:

def add(x: T, y: T): T = ...

Better yet, you can just overload the + method:

def +(x: T, y: T): T = ...

There's a missing piece, though, which is the type parameter itself. As written, the method is missing its class. The most likely case is that you're calling the + method on an instance of T, passing it another instance of T. I did this recently, defining a trait that said, "an additive group consists of an add operation plus the means to invert an element"

trait GroupAdditive[G] extends Structure[G] {
  def +(that: G): G
  def unary_- : G
}

Then, later, I define a Real class that knows how to add instances of itself (Field extends GroupAdditive):

class Real private (s: LargeInteger, err: LargeInteger, exp: Int) extends Number[Real] with Field[Real] with Ordered[Real] {
  ...

  def +(that: Real): Real = { ... }

  ...
}

That may be more than you really wanted to know right now, but it does show both how to define generic arguments and how to realize them.

Ultimately, the specific types aren't required, but the compiler does need to know at least the type bounds.