Scala: Abstracting the toString function of case classes

时间:2019-03-19 14:39:47

标签: scala inheritance typeclass shapeless case-class

Suppose I have a case class defined as follows:

case class User(name: String, age: Int)

I would like to override its toString method like this:

 case class User(name: String, age: Int) {
    override def toString: String = 
      s"name = $name
        age = $age"

So that if I run print(user.toString), I get:

name = nameOfUser
age = ageOfUser

Now I have another class called Computer and it is defined as

case class Computer(make: String, RAM: int, clockSpeed: Double)

I would like to print the same for all its values. I would like something like:

  make = makeOfComputer
  RAM = RAMOfComputer
  clockSpeed = speedOfComputer

Instead of copying, pasting and adapting the above toString function to the Computer class, I'd rather abstract this away so that it can be used by any case class.

Some ideas I have are that I can use

 CaseClassType.unapply(caseClassInstance).get.productIterator.toList

to get the values of the case class and

 classOf[CaseClass].getDeclaredFields.map(_.getName)

to get the names of the fields. What this means is that I can find a list of all the values in a case class as well as a list of all the field names without knowing the actual structure of the case class at all.

Once I have these two collections, I can recursively go through them to create the string. I was thinking something like the below could work but unfortunately scala doesn't allow case classes to inherit from other case classes.

case class StringifiableCaseClass(){
  override def toString: String =
     //get the values and the fieldnames and create a string

All I'd have to do is get the case classes to extend StringifiableCaseClass and they would be able to create the right string.

The reason why I want to use a case class as the base type is that I can use

 classOf[this.type]...etc

and

  this.type.unapply(this)...etc

in StringifiableCaseClass.

Any ideas on how I can achieve what I am after given I cannot extend case classes with other case classes?

2 个答案:

答案 0 :(得分:5)

基本实现不必是case类。只需使其成为一个特征,然后您的case类就可以对其进行扩展。

 trait Stringification { self: Product => 
    override def toString() = 
      getClass.getDeclaredFields
      .zip(productIterator.toSeq)
      .map { case (a, b) => s"${a.getName}=$b" }
      .mkString("\n")
 }

然后

  case class Foo(foo: String, bar: Int) extends Stringification
  case class Bar(foo: Foo, bar: String) extends Stringification

...等等

答案 1 :(得分:2)

If I understand your use-case correctly, you can use Cats default Show type-class or, if you need more custom behaviour, you can directly use generic derivation with Shapeless to achieve what you are after.

For the easier scenario of Cat's Show, read here.

As you can read, it has syntax that will let you do myObject.show on any T that has a Show[T] in scope.

If you want a more customisable behaviour, you can try going directly with Shapeless. An example of what you need can be found in this example Show type-class and you can see it running here.

Given these case classes:

  sealed trait Super
  case class Foo(i: Int, s: String) extends Super
  case class Bar(i: Int) extends Super
  case class BarRec(i: Int, rec: Super) extends Super

  sealed trait MutualA
  case class MutualA1(x: Int) extends MutualA
  case class MutualA2(b: MutualB) extends MutualA

  sealed trait MutualB
  case class MutualB1(x: Int) extends MutualB
  case class MutualB2(b: MutualA) extends MutualB

It will print:

Bar(i = 0)   
BarRec(i = 1, rec = Foo(i = 0, s = foo))   
MutualA2(b = MutualB2(b = MutualA1(x = 0)))