'Declaring a method that uses the subtype class in its signature?

I have defined a trait Mergeable that represents a value that can be merged with another of its kind, but I'm having trouble declaring the types correctly.

trait Mergeable[T] {
  def value: Option[T]
  def merge(other: ???): ???
}

trait SummingInt extends Mergeable[Int] {
  def merge(other: SummingInt): SummingInt = {
    val sum = for {
      i1 <- value
      i2 <- other.value
    } yield i1 + i2
    sum orElse this orElse other
  }
}

trait MultiplyingInt extends Mergeable[Int] {
  def merge(other: MultiplyingInt): MultiplyingInt = {
    val product = for {
      i1 <- value
      i2 <- other.value
    } yield i1 * i2
    product orElse this orElse other
  }
}

I want to require that all Mergeable define a method merge that merges it with another of it's own kind. So it is illegal to merge a SummingInt with a MultiplyingInt.

If I choose:

def merge(other: Mergeable[T]): Mergeable[T]

Then I lose that guarantee of only merging like-with-like. But if I don't define merge on Mergeable, then I cannot write this method which would make my life a whole lot simpler:

def mergeTwo[T <: Mergeable](first: T, second: T) = first merge second

Even though mergeTwo can guarantee that first and second are of the same type and thus are safe to merge!

Does the Scala language make it at all possible for me to define merge at the level of Mergeable?



Solution 1:[1]

To elaborate on the comments, what you're looking for is not a subclass but a typeclass. It's not accurate to say that your values are instances of Mergeable, because that implies some sort of "global" mergeable functionality that just isn't realistic. Instead, it makes sense to say that Mergeable is a capability that can be applied to types. That is, "The type Int is a Mergeable", not "Every Int is itself mergeable".

I'll use the Scala 2 syntax here, since you haven't mentioned a Scala version. Note that some of the syntax has changed in Scala 3.

We use a trait to describe our typeclass. Crucially, this trait is not going to be implemented by the concrete types like Int or String. It's going to be implemented by special singleton objects we make up.

trait Mergeable[A] {
  def merge(lhs: A, rhs: A): A
}

Now we provide instances for known types using the 'implicit'. keyword. We would use given in Scala 3.

implicit object IntIsMergeable extends Mergeable[Int] {
  def merge(lhs: Int, rhs: Int) = lhs + rhs
}

implicit object StringIsMergeable extends Mergeable[String] {
  def merge(lhs: String, rhs: String) = lhs + rhs
}

// Note: 'class' here since it's parameterized so it's not
// just one object.
implicit class ListIsMergeable[A] extends Mergeable[List[A]] {
  def merge(lhs: List[A], rhs: List[A]) = lhs ++ rhs
}

Now, if we want to write a function that takes mergeable objects and does something, we use implicit arguments. Let's say we wanted to write a function of three arguments that merges those three.

def merge3[A](x: A, y: A, z: A)(implicit mergeable: Mergeable[A]): A =
    mergeable.merge(mergeable.merge(x, y), z)

merge3 is a function of four arguments: x, y, z, and mergeable. When we call merge3, we may (at our option) omit the fourth argument and Scala will just kind of automagically find one with the right type. So if we pass three integers, it knows it needs a Mergeable[Int], and there's only one implicit instance of that around: the IntIsMergeable we wrote earlier. There's a lot of science going into how to resolve instances efficiently and intuitively, but that's the basic idea.

There are also a lot of conventions in place for writing good typeclasses people will understand, such as providing an apply that invokes an implicit instance and providing extension methods. The Shapeless book goes into detail on some of these conventions if it's something you're interested in.

Finally, as hinted at in the comments, your Mergeable already has a name: it's called Semigroup. It's a well-understood mathematical object and is provided by both Scalaz and Cats.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Silvio Mayolo