Scala - 如何像(漂亮打印的)树一样打印case类

61

我正在使用Scala Combinators制作解析器,非常棒。最终得到的是一长串交织在一起的case类,例如:ClassDecl(Complex,List(VarDecl(Real,float), VarDecl(Imag,float))),长度要达到原来的100倍。我想知道有没有好的方法可以以树形式打印这些case类,让它更易读?(或其他形式的漂亮打印)

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

我希望最终得到类似这样的结果

编辑:奖励问题

是否有一种方法可以显示参数的名称..?像这样:ClassDecl(name=Complex, fields=List(...)

11个回答

38

请查看名为 sext 的小型扩展库。它导出了这两个函数,正好可用于此类目的。

以下是如何将其用于您的示例的方法:

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)

}

以下是此程序的输出:

treeString output:

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

valueTreeString output:

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

1
这很好!(也适用于Scala 2.11)。如何为给定类型(例如您的示例中的VarDecl)自定义treeString或valueTreeString?我尝试了这个: implicit class SextVarDecl(val vd: VarDecl) extends SextAnyTreeString[VarDecl](vd) { override def treeString: String = s"VarDecl(a=${vd.a}}, b='${vd.b}')" } - Nicolas Rouquette
@NikitaVolkov 有没有类似的库可以将普通类以树形方式打印出来? - RaAm
1
Sext目前似乎已经不再维护了! - Hartmut Pfarr
我正在尝试在Java 12 / Scala 2.13项目中使用Sext,但是我遇到了java.lang.NoClassDefFoundError: scala/collection/SeqLike错误。为什么会这样?Sext代码中没有对SeqLike的引用吗? - TomR

21

从Scala 2.13开始,case class(它们是Product的实现)现在提供了一个productElementNames方法,该方法返回一个迭代器,包含它们字段的名称。

结合Product::productIterator提供的case class值,我们有了一种简单的方式来漂亮地打印case classes,而无需使用反射:

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 _ =>
  }
}

针对您的具体情况:

// 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)

产生:

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

4
我无耻地复制了你的答案,然后发布了一个。如果你在意我这样使用你的代码,请告诉我,我会进行修改或删除。请您联系我。 - Mario Galic

16

使用com.lihaoyi.pprint库。

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

val data = ...

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

您还可以配置宽度、高度、缩进和颜色:

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

文档:https://github.com/com-lihaoyi/PPrint


4
谢谢,但不幸的是它没有考虑到大小写类字段的名称。 - Albert Bikeev
什么是进口? - samthebest

9
这是我改进的解决方案,大大提高了http://www.lihaoyi.com/PPrint/对于 case-classes 的处理能力(请参见https://github.com/lihaoyi/PPrint/issues/4)。例如,这将打印出以下内容: enter image description here 对于以下用例:
  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)

代码:

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

太棒了,谢谢!但是你为什么要以这种方式创建pprint2对象呢?为什么不只是在PPrintUtils对象的末尾定义它作为“val”呢? - Albert Bikeev
你考虑过将它提交回 PPrint 吗? - tribbloid

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

这里的目标不是提供一个通用的对象美化打印方案,而主要是为了打印域对象(case class)并将其直接粘贴到测试用例中。Scala(相对于Haskell)的问题在于它们不以“源代码”形式打印。 - F. P. Freely
@mauriciojost 谢谢您。 - F. P. Freely
你的字符串大小写出了问题,因为你使用了 foldLeft,所以你最终会替换掉之前的替换。尝试使用 val string = "foo\\\"bar" 进行调试。 - samthebest

7

就像解析器组合器一样,Scala已经在标准库中包含了漂亮的打印机组合器。(注意:自Scala 2.11起,该库已被弃用。类似的漂亮打印库是kiama开源项目的一部分)。

你的问题没有明确说明你需要“反射”的解决方案还是你想要显式构建打印机。(尽管你的“奖励问题”暗示你可能想要“反射”解决方案)。

无论如何,如果您想使用普通Scala库开发简单的漂亮打印机,下面是代码。以下代码可以在REPL中运行。

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)
))
奖励问题: 将打印机包装成类型类,实现更高的可组合性。

5
这些内容在Scala 2.11中已被弃用;请参见https://groups.google.com/forum/#!topic/scala-language/e7CqLqlxLts。 - Seth Tisue

5

我发现最好、最简洁的“开箱即用”体验是使用Kiama漂亮打印库。它不需要使用额外的组合器就可以打印成员名称,只需要使用import org.kiama.output.PrettyPrinter._; pretty(any(data)),您就有了一个很好的开始:

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)

产生:

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

请注意,这只是最基本的示例。Kiama PrettyPrinter 是一个极其强大的库,具有丰富的组合器集合,专门用于智能空格、换行、嵌套和分组。非常容易根据您的需求进行调整。截至本篇文章发布时,在 SBT 中可用:
libraryDependencies += "com.googlecode.kiama" %% "kiama" % "1.8.0"

pretty本身并不会打印任何内容,它必须提供给println或类似的函数。它也不会打印属性名称,因此与case类的标准toString相比,输出提供的价值很小。 - Abhijit Sarkar
很遗憾,库链接已被删除(2020年)。 - Hartmut Pfarr

4

使用反射

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
  }
}

4

这是对@F. P Freely的无耻抄袭,但我:

  • 添加了缩进功能
  • 稍作修改以使输出符合Scala风格(并且适用于所有原始类型)
  • 修复了字符串文字错误
  • 添加了对java.sql.Timestamp的支持(因为我经常在Spark中使用它)

完成!

// 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)
  }

例如。
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
)

2
如果您使用Apache Spark,可以使用以下方法打印您的case类:
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)))
}

这将为您的样例类实例提供一个格式良好的JSON表示。请确保已导入sparkSession.implicits._
示例:
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)

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接