简体   繁体   English

Scala - 如何打印案例类,如(漂亮打印的)树

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

I'm making a parser with Scala Combinators.我正在用 Scala 组合器制作一个解析器。 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.我最终得到的是一长串纠缠的案例类,例如: ClassDecl(Complex,List(VarDecl(Real,float), VarDecl(Imag,float))) ,只是长了 100 倍。 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 ) (或其他形式的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(... ) ?比如: ClassDecl(name=Complex, fields=List(... )

Check out a small extensions library named sext .查看一个名为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 class es (which are an implementation of Product ) are now provided with a productElementNames method which returns an iterator over their field's names.Scala 2.13开始, case class es(它是Product的实现)现在提供了一个productElementNames方法,该方法返回一个迭代器的字段名称。

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 :结合提供案例类值的Product::productIterator ,我们有一种简单的方法来漂亮地打印案例类而无需反射

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.使用 com.lihaoyi.pprint 库。

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文档: 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 ).这是我的解决方案,它极大地改进了http://www.lihaoyi.com/PPrint/处理案例类的方式(请参阅https://github.com/lihaoyi/PPrint/issues/4 )。

eg it prints this:例如它打印这个: 在此处输入图片说明

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

Just like parser combinators, Scala already contains pretty printer combinators in the standard library.就像解析器组合器一样,Scala 已经在标准库中包含了漂亮的打印机组合器。 ( note : this library is deprecated as of Scala 2.11. A similar pretty printing library is a part of kiama open source project). 注意:这个库从 Scala 2.11 开始被弃用。一个类似的漂亮的打印库是kiama开源项目的一部分)。

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.不管怎样,如果你想使用普通的 Scala 库开发简单漂亮的打印机,这里是。 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.额外问题:将打印机包装到类型类中以获得更大的可组合性。

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

The nicest, most concise "out-of-the" box experience I've found is with the Kiama pretty printing library .我发现的最好、最简洁的“开箱即用”体验是使用Kiama 漂亮的打印库 It doesn't print member names without using additional combinators, but with only import org.kiama.output.PrettyPrinter._; pretty(any(data))它不会在不使用其他组合器的情况下打印成员名称,而是仅使用import org.kiama.output.PrettyPrinter._; pretty(any(data)) import org.kiama.output.PrettyPrinter._; pretty(any(data)) you have a great start: 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)

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. Kiama PrettyPrinter 是一个非常强大的库,具有一组丰富的组合器,专为智能间距、换行、嵌套和分组而设计。 It's very easy to tweak to suit your needs.调整以满足您的需求非常容易。 As of this posting, it's available in SBT with:在这篇文章中,它在 SBT 中可用:

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

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

This is a shamless copy paste of @F.这是@F 的无耻复制粘贴。 P Freely, but P 自由,但是

  • 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)稍作修改,使输出具有正确的 Scala 风格(并将为所有原始类型编译)
  • Fixed string literal bug修复了字符串文字错误
  • Added support for java.sql.Timestamp (as I use this with Spark a lot)添加了对java.sql.Timestamp支持(因为我java.sql.Timestamp在 Spark 中使用它)

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

Eg例如

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
)

If you use Apache Spark, you can use the following method to print your case classes :如果您使用 Apache Spark,您可以使用以下方法打印您的案例类:

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.这为您的案例类实例提供了格式良好的 JSON 表示。 Make sure sparkSession.implicits._ is imported确保导入sparkSession.implicits._

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

I would suggest using the same print that is used in the AssertEquals of your test framework of choice.我建议使用与您选择的测试框架的 AssertEquals 中使用的相同的打印。 I was using Scalameta and munit.Assertions.munitPrint(clue: => Any): String does the trick.我正在使用Scalametamunit.Assertions.munitPrint(clue: => Any): String来解决问题。 I can pass nested classes to it and see the whole tree with the proper indentation.我可以将嵌套类传递给它,并以适当的缩进查看整棵树。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM