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

ScalaPretty PrintCase Class

Scala Problem Overview


I'm making a parser with Scala Combinators. It is awesome. What I end up with is a long list of entagled case classes, like: ClassDecl(Complex,List(VarDecl(Real,float), VarDecl(Imag,float))), just 100x longer. I was wondering if there is a good way to print case classes like these in a tree-like fashion so that it's easier to read..? (or some other form of Pretty Print)

ClassDecl
  name = Complex
  fields =
  - VarDecl
      name = Real
      type = float
  - VarDecl
      name = Imag
      type = float

^ I want to end up with something like this

edit: Bonus question

Is there also a way to show the name of the parameter..? Like: ClassDecl(name=Complex, fields=List( ... ) ?

Scala Solutions


Solution 1 - Scala

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

Solution 2 - Scala

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

Solution 3 - Scala

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

Solution 4 - Scala

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(...))

Solution 5 - Scala

Just like parser combinators, Scala already contains pretty printer combinators in the standard library. (note: this library is deprecated as of Scala 2.11. A similar pretty printing library is a part of kiama open source project).

You are not saying it plainly in your question if you need the solution that does "reflection" or you'd like to build the printer explicitly. (though your "bonus question" hints you probably want "reflective" solution)

Anyway, in the case you'd like to develop simple pretty printer using plain Scala library, here it is. The following code is REPLable.

case class VarDecl(name: String, `type`: String)
case class ClassDecl(name: String, fields: List[VarDecl])

import scala.text._
import Document._

def varDoc(x: VarDecl) =
  nest(4, text("- VarDecl") :/:
    group("name = " :: text(x.name)) :/:
    group("type = " :: text(x.`type`))
  )

def classDoc(x: ClassDecl) = {
  val docs = ((empty:Document) /: x.fields) { (d, f) => varDoc(f) :/: d }
  nest(2, text("ClassDecl") :/:
    group("name = " :: text(x.name)) :/:
    group("fields =" :/: docs))
}

def prettyPrint(d: Document) = {
  val writer = new java.io.StringWriter
  d.format(1, writer)
  writer.toString
}

prettyPrint(classDoc(
  ClassDecl("Complex", VarDecl("Real","float") :: VarDecl("Imag","float") :: Nil)
))

Bonus question: wrap the printers into type classes for even greater composability.

Solution 6 - Scala

import java.lang.reflect.Field
...

/**
  * Pretty prints case classes with field names.
  * Handles sequences and arrays of such values.
  * Ideally, one could take the output and paste it into source code and have it compile.
  */
def prettyPrint(a: Any): String = {
  // Recursively get all the fields; this will grab vals declared in parents of case classes.
  def getFields(cls: Class[_]): List[Field] =
    Option(cls.getSuperclass).map(getFields).getOrElse(Nil) ++
        cls.getDeclaredFields.toList.filterNot(f =>
          f.isSynthetic || java.lang.reflect.Modifier.isStatic(f.getModifiers))
  a match {
    // Make Strings look similar to their literal form.
    case s: String =>
      '"' + Seq("\n" -> "\\n", "\r" -> "\\r", "\t" -> "\\t", "\"" -> "\\\"", "\\" -> "\\\\").foldLeft(s) {
        case (acc, (c, r)) => acc.replace(c, r) } + '"'
    case xs: Seq[_] =>
      xs.map(prettyPrint).toString
    case xs: Array[_] =>
      s"Array(${xs.map(prettyPrint) mkString ", "})"
    // This covers case classes.
    case p: Product =>
      s"${p.productPrefix}(${
        (getFields(p.getClass) map { f =>
          f setAccessible true
          s"${f.getName} = ${prettyPrint(f.get(p))}"
        }) mkString ", "
      })"
    // General objects and primitives end up here.
    case q =>
      Option(q).map(_.toString).getOrElse("¡null!")
  }
}

Solution 7 - Scala

The nicest, most concise "out-of-the" box experience I've found is with the Kiama pretty printing library. It doesn't print member names without using additional combinators, but with only import org.kiama.output.PrettyPrinter._; pretty(any(data)) you have a great start:

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")))
import org.kiama.output.PrettyPrinter._

// `w` is the wrapping width. `1` forces wrapping all components.
pretty(any(data), w=1)

Produces:

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

Note that this is just the most basic example. Kiama PrettyPrinter is an extremely powerful library with a rich set of combinators specifically designed for intelligent spacing, line wrapping, nesting, and grouping. It's very easy to tweak to suit your needs. As of this posting, it's available in SBT with:

libraryDependencies += "com.googlecode.kiama" %% "kiama" % "1.8.0"

Solution 8 - Scala

Using reflection

import scala.reflect.ClassTag
import scala.reflect.runtime.universe._

object CaseClassBeautifier  {
  def getCaseAccessors[T: TypeTag] = typeOf[T].members.collect {
    case m: MethodSymbol if m.isCaseAccessor => m
  }.toList

  def nice[T:TypeTag](x: T)(implicit classTag: ClassTag[T]) : String = {
    val instance = x.asInstanceOf[T]
    val mirror = runtimeMirror(instance.getClass.getClassLoader)
    val accessors = getCaseAccessors[T]
    var res = List.empty[String]
    accessors.foreach { z ⇒
      val instanceMirror = mirror.reflect(instance)
      val fieldMirror = instanceMirror.reflectField(z.asTerm)
      val s = s"${z.name} = ${fieldMirror.get}"
      res = s :: res
    }
    val beautified = x.getClass.getSimpleName + "(" + res.mkString(", ") + ")"
    beautified
  }
}

Solution 9 - Scala

This is a shamless copy paste of @F. P Freely, but

  • I've added an indentation feature
  • slight modifications so that the output will be of correct Scala style (and will compile for all primative types)
  • Fixed string literal bug
  • Added support for java.sql.Timestamp (as I use this with Spark a lot)

Tada!

// Recursively get all the fields; this will grab vals declared in parents of case classes.
  def getFields(cls: Class[_]): List[Field] =
    Option(cls.getSuperclass).map(getFields).getOrElse(Nil) ++
      cls.getDeclaredFields.toList.filterNot(f =>
        f.isSynthetic || java.lang.reflect.Modifier.isStatic(f.getModifiers))
  
  // FIXME fix bug where indent seems to increase too much
  def prettyfy(a: Any, indentSize: Int = 0): String = {
    val indent = List.fill(indentSize)(" ").mkString

    val newIndentSize = indentSize + 2
    (a match {
      // Make Strings look similar to their literal form.
      case string: String =>
        val conversionMap = Map('\n' -> "\\n", '\r' -> "\\r", '\t' -> "\\t", '\"' -> "\\\"", '\\' -> "\\\\")
        string.map(c => conversionMap.getOrElse(c, c)).mkString("\"", "", "\"")
      case xs: Seq[_] =>
        xs.map(prettyfy(_, newIndentSize)).toString
      case xs: Array[_] =>
        s"Array(${xs.map(prettyfy(_, newIndentSize)).mkString(", ")})"
      case map: Map[_, _] =>
        s"Map(\n" + map.map {
          case (key, value) => "  " + prettyfy(key, newIndentSize) + " -> " + prettyfy(value, newIndentSize)
        }.mkString(",\n") + "\n)"
      case None => "None"
      case Some(x) => "Some(" + prettyfy(x, newIndentSize) + ")"
      case timestamp: Timestamp => "new Timestamp(" + timestamp.getTime + "L)"
      case p: Product =>
        s"${p.productPrefix}(\n${
          getFields(p.getClass)
            .map { f =>
              f.setAccessible(true)
              s"  ${f.getName} = ${prettyfy(f.get(p), newIndentSize)}"
            }
            .mkString(",\n")
        }\n)"
      // General objects and primitives end up here.
      case q =>
        Option(q).map(_.toString).getOrElse("null")
    })
      .split("\n", -1).mkString("\n" + indent)
  }

E.g.

case class Foo(bar: String, bob: Int)

case class Alice(foo: Foo, opt: Option[String], opt2: Option[String])

scala> prettyPrint(Alice(Foo("hello world", 10), Some("asdf"), None))
res6: String =
Alice(
  foo = Foo(
    bar = "hello world",
    bob = 10
  ),
  opt = Some("asdf"),
  opt2 = None
)

Solution 10 - Scala

If you use Apache Spark, you can use the following method to print your case classes :

def prettyPrint[T <: Product : scala.reflect.runtime.universe.TypeTag](c:T) = {
  import play.api.libs.json.Json
  println(Json.prettyPrint(Json.parse(Seq(c).toDS().toJSON.head)))
}

This gives a nicely formatted JSON representation of your case class instance. Make sure sparkSession.implicits._ is imported

example:

case class Adress(country:String,city:String,zip:Int,street:String) 
case class Person(name:String,age:Int,adress:Adress) 
val person = Person("Peter",36,Adress("Switzerland","Zürich",9876,"Bahnhofstrasse 69"))

prettyPrint(person)

gives :

{
  "name" : "Peter",
  "age" : 36,
  "adress" : {
    "country" : "Switzerland",
    "city" : "Zürich",
    "zip" : 9876,
    "street" : "Bahnhofstrasse 69"
  }
}

Solution 11 - Scala

I would suggest using the same print that is used in the AssertEquals of your test framework of choice. I was using Scalameta and munit.Assertions.munitPrint(clue: => Any): String does the trick. I can pass nested classes to it and see the whole tree with the proper indentation.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionkornfridgeView Question on Stackoverflow
Solution 1 - ScalaNikita VolkovView Answer on Stackoverflow
Solution 2 - ScalaXavier GuihotView Answer on Stackoverflow
Solution 3 - ScalaDavid PortabellaView Answer on Stackoverflow
Solution 4 - Scalauser1276782View Answer on Stackoverflow
Solution 5 - ScalaAlexander AzarovView Answer on Stackoverflow
Solution 6 - ScalaF. P. FreelyView Answer on Stackoverflow
Solution 7 - ScalametasimView Answer on Stackoverflow
Solution 8 - ScalapiotrView Answer on Stackoverflow
Solution 9 - ScalasamthebestView Answer on Stackoverflow
Solution 10 - ScalaRaphael RothView Answer on Stackoverflow
Solution 11 - ScalaFrancisco López-SanchoView Answer on Stackoverflow