Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!")
  }
}

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.