4

In a Scala typeclass, there will be a trait on which operations are defined, e.g. NumberLike with plus() and minus(), Transformer with transform(), or AddressLabelMaker with toLabel(). It's then possible to extend the trait with typeclass members.

Typically the operations will have the same number of parameters for different members, i.e. the signatures will look very similar. My question is: what happens if a member needs what is essentially the same operation, but with an extra parameter (maybe just an implicit one: something that modifies the operation based on the context) ?

Is there a way to do this without defining a whole new (similar) typeclass?

James McCabe
  • 1,879
  • 2
  • 15
  • 22

3 Answers3

2

With an extra explicit parameter? No. Each typeclass instance must have the same interface, that of the typeclass. With an implicit parameter? Yes. You can declare an implicit def that returns the required implicit but itself requires an implicit. Here is an example to explain:

case class Cat(name: String)
case class Person(name: String)
case class Silverware(kind: String)

implicit object GoodSilver extends Silverware("the good silver")

trait AnimalFeeder[A] {
  def feed(animal: A): Unit
}

implicit object CatFeeder extends AnimalFeeder[Cat] {
  def feed(cat: Cat) = println(cat.name + " eats cat food!")
}

implicit def personFeeder(implicit silverware: Silverware) =
  new AnimalFeeder[Person] {
    def feed(person: Person) =
      println(person.name + " eats people food with " + silverware.kind)
  }

def feedAnimal[A](a: A)(implicit feeder: AnimalFeeder[A]) = feeder.feed(a)

CatFeeder provides an implicit that cat feed any Cat. personFeeder is a def that can create an implicit to feed a Person, but requires an implicit Silverware. So in an invocation like:

feedAnimal(Person("John"))

The compiler will search for an implicit AnimalFeeder[Person], will find personFeeder, and then will search for an implicit Silverware, finally finding GoodSilver.

It is important to note that personFeeder is not an implicit conversion. Despite being a method from Silverware to AnimalFeeder[Person] it will never implicitly convert Silverware. This is because its parameter is marked implicit, implicit conversions must have their parameter explicit.

wingedsubmariner
  • 13,350
  • 1
  • 27
  • 52
  • I think this is a very clever solution, but just wondered what you think about the other solution I suggested: encapsulating the parameter list to keep the same interface for all typeclass instances? – James McCabe Jul 24 '14 at 17:09
  • If I understand you correctly, that could work very well. You might want to add example code to your answer, it would make it easier to understand. – wingedsubmariner Jul 24 '14 at 17:19
0

The way I am solving this is to wrap the parameters in a type Params, which has subtypes as necessary. Params becomes the second type parameter of the typeclass. So now members of the typeclass can have the same signature for operations.

Wonder if this is a common solution?

Here is what an example of what I am getting at. I'm not sure though, maybe this code can be improved.

trait Animal
case class Cat(name: String) extends Animal
case class Person(name: String) extends Animal
case class Silverware(kind: String)

trait FeederParams
case class CatFeederParams() extends FeederParams
case class PersonFeederParams(val silverware: Silverware) extends FeederParams

trait AnimalFeeder[A <: Animal, P <: FeederParams] {
  def feed(animal: A)(implicit params: P): Unit
}

implicit object CatFeeder extends AnimalFeeder[Cat, CatFeederParams] {
  def feed(cat: Cat)(implicit params: CatFeederParams) =
    println(cat.name + " eats cat food!")
}

implicit object PersonFeeder extends AnimalFeeder[Person, PersonFeederParams] {
  def feed(person: Person)(implicit params: PersonFeederParams) =
    println(person.name + " eats people food with " + params.silverware.kind)
}

def feedAnimal[A <: Animal, P <: FeederParams](a: A)(implicit feeder: AnimalFeeder[A, P], params: P) =
  feeder.feed(a)

implicit object personParams extends PersonFeederParams(Silverware("the good silver"))
implicit object catParams extends CatFeederParams()
feedAnimal(Person("John"))
feedAnimal(Cat("Garfield"))
James McCabe
  • 1,879
  • 2
  • 15
  • 22
  • This is the solution I picked to use in the Libanius project, and it seems to be working out. But I would still be interested in hearing from anybody who sees a problem with this approach, or knows of other places where something similar has been done. Of course I think wingedsubmariner had a good solution too. – James McCabe Aug 07 '14 at 20:17
0

I'm not sure what you mean to question, but if it's about changing what a typeclass' instance's methods do, I think it should be possible, by changing the instance's condition somehow before you call the methods.

Given a type class Printer[A]:

trait Printer[A] {
  def print(a: A): Unit
}
def print[A: Printer](a: A) = implicitly[Printer[A]].print(a)

Defining an implicit Printer[Int], named IntPrinter:

implicit object IntPrinter extends Printer[Int] {
  def print(a: Int) = s"integer $a"
}
print(3) //=> integer 3

Modifying IntPrinter so it can change the text to print:

implicit object IntPrinter extends Printer[Int] {
  var text: String = "integer "
  def print(a: Int) = s"$text$a"
}
print(3) //=> integer 3
IntPrinter.text = ":"
print(3) //=> :3

Thus you can control the behavior of typeclass` methods.

Ryoichiro Oka
  • 1,949
  • 2
  • 13
  • 20