Companion object and generics

#1

Hi,

I’m new to Scala and having some trouble with generics and companion objects. I have tried to find the answer elsewhere but have’t found anything which exactly matches my problem.

I have some (simplified) code like this:

class MyInt(val num: Int) {

def +(that: MyInt): MyInt = new MyInt(num + that.num)
def *(that: MyInt): MyInt = new MyInt(num * that.num)
}

object MyInt {

val zero = new MyInt(0)
val one = new MyInt(1)
}

I am interested in abstracting this behaviour using a generic trait Ring[A]. I can guarantee the existence of the + and * methods in MyInt by defining:

trait Ring[A] {
def +(that: A): A
def *(that: A): A
}

and then I can say that MyInt extends Ring[MyInt]. However, if I was writing some generic code for a type A which extended Ring[A], I could not write A.zero and A.one and have the code compile. Is there a way to guarantee that if a class A extends Ring[A] then the companion object to A exists and contains val’s zero and one (so A.zero and A.one make sense).

Thanks in advance for any help.

#2

Have you looked into typeclasses yet? At first blush, that’s how I would probably approach this problem: I would define Ring as a typeclass along the lines of:

trait Ring[T] {
  def +(t: A, that: A): A
  def *(t: A, that: A): A
  def zero: A
  def one: A
}

Then you would implement Ring[T] for each desired type, like this (haven’t tried compiling this, but something like it) (edited to actually get this right):

object Ring {
  implicit val MyIntRing = new Ring[MyInt] {
    def +(t: MyInt, that: MyInt): MyInt = new MyInt(t.num + that.num)
    def *(t: MyInt, that: MyInt): MyInt = new MyInt(t.num * that.num)
    val zero = new MyInt(0)
    val one = new MyInt(1)
  }

  implicit class RingSyntax[T: Ring](t: T) {
    def +(that: A): A = implicitly[Ring[T]].+(t, that)
    def *(that: A): A = implicitly[Ring[T]].*(t, that)
  }
}

The typeclass approach – implementing the desired functionality from outside the type, instead of via inheritance – is very common in idiomatic Scala, and often makes it pretty easy to deal with problems that are challenging (or impossible) through inheritance…

2 Likes
#3

This definitely looks like a use case for type classes, because they, in contrast to polymorphism via inheritance, allow for methods that don’t require an instance of the type in question (like your one and zero).

@jducoeur already gave a possible Ring type class with an implementation for MyInt. The implicit class RingSyntax will allow you to use the operators on any type A, for which there exists an implicit Ring[A] in scope.

Note, that it uses different syntax for the type parameter constraint: when using inheritance, you’d write something like [A <: Ring[A], meaning A has to extend Ring[A].
The [A: Ring] syntax is called a context bound and is syntactic sugar for requiring an implicit instance of Ring[A] defined separately from A.

So if you want to write methods working with any type for which there is a Ring, you can do it this way:

import Ring._
def sum[A: Ring](x: A, y: A): A = x + y

For using zero/one:

def addOne[A: Ring] (a: A): A =
    a + implicitly[Ring[A]].one

the implicitly looks up the implicit instance of Ring[A], which is guaranteed to exist because of the context bound.

In a case like this, where you need to use the instance of Ring explicitly, you can also use an implicit parameter instead of a context bound (which is the same under the hood):

def addOne[A](a: A)(implicit ring: Ring[A]): A =
    a + ring.one

As a bonus, as you can define typeclasses separately from the type in question, you could also define Ring[Int] and have the same methods work with builtin types.

2 Likes
#4

Thanks for the code. Very helpful.

As an alternative to subtype polymorphism, are there limitations? For instance, can code process a collection of ring values of different types, given the lack of a common super type?

Using usual polymorphism:

trait Ring[A] {
  def unary_- : A
}

class MyInt extends Ring[MyInt] {
  def unary_-  = this
}
class MyReal extends Ring[MyReal] {
  def unary_-  = this
}

val l: List[Ring[_]] = List(new MyInt, new MyReal)

l.map(x => -x)

Can I do such a thing with typeclasses? I’m trying to redo the classic example where a list of values of type Shape are drawn by calling draw on them. Can that be done with typeclasses instead? I can certainly define a type bound Shape and types Square and Circle and have a method that takes any kind of shape and draws it, but what about a list of shapes of different types?

#5

For the specific example, you could give Ring and RingOps an unary_- and take a List[RingOps[_]]

As a rule of thumb, if you want to replace a “self method” – e.g. A#foo with a method on Fooable[A].foo(a: A), then you need a forwarder class like RingOps of the format

class WithRing[A](a: A, tc: Ring[A]) {
}

then you can have the List[WithRing[_]]

#6

This is confusing. I assume RingOps is what RingSyntax was in the other post. But then WithRing is almost the same thing, except that the Ring[A] argument is explicit, which will make creating instance of WithRing awkward.

Also, does this mean that RingOps, an implicit class, needs to be instantiated explicitly to create the list, as in List(new RingOps(new MyInt), new RingOps(new MyReal))?

#7

Providing instances like one and zero is a strong type class use case. But providing operators like -, + * is a strong inheritance use case.

#8

The point of WithRing is to keep both the data and the Ring operations.

Remember, in the typeclass world, your typeclass’ operations are separate from the data. You have your type T, and you have a collection of operations, Ring[T]. All of which is great. But a Ring[_] is useless – indeed, almost meaningless – because it’s a bunch of operations divorced from the data types that they are designed to operate on. The compiler doesn’t know what data you are allowed to mix with the Ring[_].

So what WithRing is doing is attaching the data T and the operations Ring[T] together. You can safely have a List of that, because each element now contains both parts, and they are known to be compatible – you can pull off each WithRing[_], and since it contains matching data and operations, they can be used together.

This is all separate from the concept of Ops / Syntax (which are synonyms, yes – they’re the two most common names for the same idea), which is just about making it easier to call the functions in Ring. Basically, an Ops class puts an OO-style gloss on top of the FP operations, so that they look more ergonomic. But it’s strictly optional, whereas the WithRing idea is more or less necessary for certain design patterns where you are potentially mixing a bunch of different Ts together…

#9

Understood. But I’m still stuck.

trait Ring[A] {
  def negate(x: A): A
}

case class MyInt(num: Int)
case class MyReal(num: Double)

implicit object MyIntRing extends Ring[MyInt] {
  def negate(x: MyInt) = MyInt(-x.num)
}

implicit object MyRealRing extends Ring[MyReal] {
  def negate(x: MyReal) = MyReal(-x.num)
}

implicit class RingOps[A : Ring](a: A) {
  def unary_- : A = implicitly[Ring[A]].negate(a)
}

val one = MyInt(1)
val two = MyReal(2.0)

So far, so good. I can write things like -one or -two.

I can bring the values together as instances of WithRing:

case class WithRing[A](value: A, ring: Ring[A])

def addRing[A : Ring](a: A): WithRing[A] = WithRing(a, implicitly[Ring[A]])

val rings: List[WithRing[_]] = List(addRing(one), addRing(two))

But how can I now negate all the values inside rings? You said:

But this doesn’t work:

for (r <- rings) {
  println(r.ring.negate(r.value))
}

as the relationship between the type of r.ring and the type of r.value seems to have been lost in the wildcard WithRing[_].

#10

Thats annoying and also, I think, a bug. I’m on mobile, so I can’t easily check, but does that fail on all scala versions? And if you write it out as a map?

Maybe you need to be able to refer to the A as a type member. That would be a bummer.

#11

Just give the compiler a “T”. It really seems to like "T"s. :wink:

def processWithRing[T](r: WithRing[T]): Unit =
  println(r.ring.negate(r.value))

for (r <- rings) {
  processWithRing(r)
}
2 Likes
#13

Good point. I should have thought of that. Same reason f2 can be defined but not f1:

def f1(s: Set[_]) = s + s.head
def f2[A](s: Set[A]) = s + s.head

Compiler needs help sometimes…

#14

The issue is fixed in dotty: https://scastie.scala-lang.org/Y5sTT8RhT2ClT7Jffw3brg

This workaround also works: https://scastie.scala-lang.org/q51B3kwnTtGKoPMb8SothA

1 Like
#15

Not related to the main topic of this question, but I noticed you wrote the implicit as a val of an anonymous class while I use an object. Any reason to prefer one to the other (not just for implicit, but in general)?

#16

Not insofar as I know – I’m prone to using them interchangeably – but others here might know of some advantages one way or t’other.

Note, though, that there are time it needs to be a def, usually because you have a type parameter involved. (For example, typeclass instances for collections usually want to be def for this reason.)

#17

the val or def are of type Ring[MyInt], while the object is a subtype. This may affect implicit priorities.

2 Likes
#18

For operations like (A, A) => A typeclasses offer better chance for symmetry than OO polymorphism. If I have two references of type A, eg val v1: A = new SomeSubclassOfA() and val v2: A = new AnotherSubclassOfA() then v1.equals(v2) can invoke completely different method than v2.equals(v1). With a typeclass instance I would invoke the same method implementation regardless if I invoke EqualA.equal(v1, v2) or EqualA.equal(v2, v1).