Scala - how to print case classes like (pretty printed) tree

Check out a small extensions library named sext. It exports these two functions exactly for purposes like that.

Here's how it can be used for your example:

object Demo extends App {

  import sext._

  case class ClassDecl( kind : Kind, list : List[ VarDecl ] )
  sealed trait Kind
  case object Complex extends Kind
  case class VarDecl( a : Int, b : String )


  val data = ClassDecl(Complex,List(VarDecl(1, "abcd"), VarDecl(2, "efgh")))
  println("treeString output:\n")
  println(data.treeString)
  println()
  println("valueTreeString output:\n")
  println(data.valueTreeString)

}

Following is the output of this program:

treeString output:

ClassDecl:
- Complex
- List:
| - VarDecl:
| | - 1
| | - abcd
| - VarDecl:
| | - 2
| | - efgh

valueTreeString output:

- kind:
- list:
| - - a:
| | | 1
| | - b:
| | | abcd
| - - a:
| | | 2
| | - b:
| | | efgh

Starting Scala 2.13, case classes (which are an implementation of Product) are now provided with a productElementNames method which returns an iterator over their field's names.

Combined with Product::productIterator which provides the values of a case class, we have a simple way to pretty print case classes without requiring reflection:

def pprint(obj: Any, depth: Int = 0, paramName: Option[String] = None): Unit = {

  val indent = "  " * depth
  val prettyName = paramName.fold("")(x => s"$x: ")
  val ptype = obj match { case _: Iterable[Any] => "" case obj: Product => obj.productPrefix case _ => obj.toString }

  println(s"$indent$prettyName$ptype")

  obj match {
    case seq: Iterable[Any] =>
      seq.foreach(pprint(_, depth + 1))
    case obj: Product =>
      (obj.productIterator zip obj.productElementNames)
        .foreach { case (subObj, paramName) => pprint(subObj, depth + 1, Some(paramName)) }
    case _ =>
  }
}

which for your specific scenario:

// sealed trait Kind
// case object Complex extends Kind
// case class VarDecl(a: Int, b: String)
// case class ClassDecl(kind: Kind, decls: List[VarDecl])

val data = ClassDecl(Complex, List(VarDecl(1, "abcd"), VarDecl(2, "efgh")))

pprint(data)

produces:

ClassDecl
  kind: Complex
  decls: 
    VarDecl
      a: 1
      b: abcd
    VarDecl
      a: 2
      b: efgh

Use the com.lihaoyi.pprint library.

libraryDependencies += "com.lihaoyi" %% "pprint" % "0.4.1"

val data = ...

val str = pprint.tokenize(data).mkString
println(str)

you can also configure width, height, indent and colors:

pprint.tokenize(data, width = 80).mkString

Docs: https://github.com/com-lihaoyi/PPrint


Here's my solution which greatly improves how http://www.lihaoyi.com/PPrint/ handles the case-classes (see https://github.com/lihaoyi/PPrint/issues/4 ).

e.g. it prints this: enter image description here

for such a usage:

  pprint2 = pprint.copy(additionalHandlers = pprintAdditionalHandlers)

  case class Author(firstName: String, lastName: String)
  case class Book(isbn: String, author: Author)
  val b = Book("978-0486282114", Author("first", "last"))
  pprint2.pprintln(b)

code:

import pprint.{PPrinter, Tree, Util}
object PPrintUtils {
  // in scala 2.13 this would be even simpler/cleaner due to added product.productElementNames
  protected def caseClassToMap(cc: Product): Map[String, Any] = {
    val fieldValues = cc.productIterator.toSet
    val fields = cc.getClass.getDeclaredFields.toSeq
      .filterNot(f => f.isSynthetic || java.lang.reflect.Modifier.isStatic(f.getModifiers))
    fields.map { f =>
      f.setAccessible(true)
      f.getName -> f.get(cc)
    }.filter { case (k, v) => fieldValues.contains(v) }
      .toMap
  }

  var pprint2: PPrinter = _

  protected def pprintAdditionalHandlers: PartialFunction[Any, Tree] = {
    case x: Product =>
      val className = x.getClass.getName
      // see source code for pprint.treeify()
      val shouldNotPrettifyCaseClass = x.productArity == 0 || (x.productArity == 2 && Util.isOperator(x.productPrefix)) || className.startsWith(pprint.tuplePrefix) || className == "scala.Some"

      if (shouldNotPrettifyCaseClass)
        pprint.treeify(x)
      else {
        val fieldMap = caseClassToMap(x)
        pprint.Tree.Apply(
          x.productPrefix,
          fieldMap.iterator.flatMap { case (k, v) =>
            val prettyValue: Tree = pprintAdditionalHandlers.lift(v).getOrElse(pprint2.treeify(v))
            Seq(pprint.Tree.Infix(Tree.Literal(k), "=", prettyValue))
          }
        )
      }
  }

  pprint2 = pprint.copy(additionalHandlers = pprintAdditionalHandlers)
}

// usage
pprint2.println(SomeFancyObjectWithNestedCaseClasses(...))