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
?
CodePudding user response:
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.