How to create a generic function that can be applied to two one or more types containing the same parameters?

Viewed 226

Let's say we have two case classes Woof & Meow

case class Meow(a: String, b: String)
case class Woof(a: String, b: String)

And we would like to create a function foo that concatenates a with b and works generically (kind-of) for both/either Woof & Meow like:

def foo[T](meowOrWoof: T) = meowOrWoof.a + meowOrWoof.b

Of course that will not compile as neither a nor b are parameters of T.

One thing I have tried, is to create a trait like:

trait Pets[T] {
    def foo[T](someClass: T): String
  }

  case class Meow(a: String, b: String) extends Pets[Meow] {
    override def foo[T](someClass: T) = a + b
  }
  case class Woof(a: String, b: String) extends Pets[Woof] {
    override def foo[T](someClass: T) = a + b
  }

But here we have to create the function multiple times even though it has the same implementation.

Is there another way we can create the function foo that works for both Meow & Woof, and concatenates a with b for either?

3 Answers

If your case classes can extend a common trait or abstract class...

abstract class AB {
  val a:String
  val b:String
}
case class Meow(a: String, b: String) extends AB
case class Woof(a: String, b: String) extends AB

def foo(ab:AB):String = ab.a + ab.b

foo(Woof("growl","bark"))  //res0: String = growlbark

You can use structural type in scala:

Welcome to Scala 2.13.1 (OpenJDK 64-Bit Server VM, Java 1.8.0_222).
Type in expressions for evaluation. Or try :help.

scala> case class Meow(a: String, b: String)
defined class Meow

scala> case class Woof(a: String, b: String)
defined class Woof

scala> type WithAB = { def a: String; def b: String }
defined type alias WithAB

scala> def foo(ab: WithAB): String = ab.a + ab.b

scala> foo(Meow("hello, ", "world"))
res1: String = hello, world

scala> foo(Woof("hello, ", "scala"))
res2: String = hello, scala

This kind of thing can be achieved using the Shapeless library. Be aware however that this is a very advanced topic in Scala programming, and that Shapeless 3 will probably be quite different from the current 2.3.3 version, as it will be based on Scala 3 (Dotty).

It works as follows: you write a generic function that is parametrized by a type variable T which is going to be the case class that you want to read the a member of. In addition, you need to pass a bunch of implicits that tell you more about the structure of T:

  • it is a case class. This is represented by the LabelledGeneric typeclass
  • that this case class has a field named a. This is represented by the Selector typeclass

The LabelledGeneric allows you to convert back and forth between T and another representation of that type called Repr. The purpose is to have a uniform representation for all case classes, which makes it possible to write generic code. This Repr type is an HList, which is basically a tuple of arbitrary length.

The Selector implicit is what allows you to access individual fields of values of type Repr. You need one of these for each field you want to access, and the second type parameter of Selector is the name of the field. Unfortunately Shapeless was written before String singleton types were added to Scala in 2.13, which is why you can't write "a" but you have to write Witness.`'a`.T instead.

A generic function that will retrieve the String field a from an abitrary case class looks as follows:

import shapeless._
import shapeless.ops.record.Selector

def getA[T, Repr <: HList](t: T)(
  implicit lg: LabelledGeneric.Aux[T, Repr],
  aselector: Selector.Aux[Repr, Witness.`'a`.T, String]
): String = {
  val repr: Repr = lg.to(t)
  aselector(repr)
}

I've glossed over many of the details here (such as the "Aux pattern", which is needed to work around the fact that you can only have one implicit parameter list and thus can't use dependent types), so you'll have to do some more research into how shapeless works to completely understand it. The best resource I know of is this book:

https://books.underscore.io/shapeless-guide/shapeless-guide.html

Once you understand the above example, you should be able to work out how to implement the function you were trying to write.

Related