【问题标题】:How to print recursive values in Scala?如何在 Scala 中打印递归值?
【发布时间】:2020-11-23 08:19:54
【问题描述】:

我正在用 Scala 编写一个 Lisp。

sealed trait Val

final case class Atom(name: String) extends Val

final case object Null extends Val

final class Cons(a: Val, d: => Val) extends Val {
  override def toString(): String = "Cons(" + a.toString() + "," + d.toString() + ")"
}

如何正确打印递归Vals? 示例:

lazy val lst:Val = new Cons(Atom("a"), {lst})
lst.toString()

我希望它的结果是#1=Cons(Atom("a"), #1)

【问题讨论】:

  • 这看起来像是一种奇怪的方式来做一个列表。例如,Cons(Null, Atom("a"))Cons(Null, Null) 是什么意思?
  • 尚未检查,但请尝试将 Cons 设为案例类。它应该为 toString (以及其他一些东西)生成实现,因此您不必编写它。我相信lazy/ ref 的名字也应该起作用。如果这不是出于学术目的,我建议重新设计您正在做的任何事情。
  • @goozez 您不能将Cons 设为案例类。案例类不能有惰性/按名称参数。
  • 如果您正在编写解释器,我会将运行时表示和输入表示分开。然后你可以写Define(RefName("#1"), Cons(Literal("a"), RefName("#1")))之类的东西,解释器在运行时可以作为递归定义处理,但不会作为内部表示递归。
  • 问题应该是“如何实现类似于 ANSI Common Lisp 的 'circle notation'”。

标签: scala functional-programming lisp


【解决方案1】:

首先,这是一个二叉树而不是列表,因为Cons的两个参数都可以是Cons

new Cons(new Cons(Atom("a"), Null), new Cons(Atom("b"),Null))

您可以使用测试来处理自引用对象:

final class Cons(a: Val, d: => Val) extends Val {
 override def toString =
    if (d == this) {
      "Cons(" + a.toString + ", self)"
    } else {
      "Cons(" + a.toString + "," + d.toString + ")"
    }
}

但是相互引用的对象仍然是个问题:

lazy val lst1:Val = new Cons(Atom("a"), lst2)
lazy val lst2 = new Cons(Atom("a"), new Cons(Atom("b"), lst1))

对于树来说,唯一明智的做法是在解析节点时标记节点并检查您是否不会重新访问节点。对于真正的列表,您可以使用双指针技术来检测循环。

【讨论】:

  • OP 没有谈到 list
  • @DmytroMitin 是的。当他说“a Lisp”时,我假设他的意思是“列表”,然后使用了一种名为 Cons 的类型。
  • OP 的意思是 Lisp 语言——显然,他们正在用 Scala 编写 Lisp 解释器。
【解决方案2】:

我有实现循环符号的经验,但在 C 中。

首先,方法不止一种。

您必须退后一步考虑:对象是否总是要使用toString 打印成文本?也就是说,你不打算拥有 I/O 流,以及一种将对象打印到流而不是将它们转换为字符串的方法吗?

如果您计划拥有 I/O 流,那么您可以使它们足够通用,以便实现字符串输出流,这可以作为将对象转换为字符串的基础。

在实现循环符号时,可以以各种方式做事。关键问题是你不想在所有东西上都贴上数字标签,不管它是否需要,因为那样看起来很难看。例如:

#1=(#2=(a b) #3=(c d) e) ;; no circularity or substructure sharing!

但在您的打印机发现最方便开始发射对象时,它必须知道:发射标签还是不发射?

要考虑的另一点是循环符号很昂贵。这就是 ANSI Lisp 指定 *print-circle* 特殊变量来启用它的原因,默认情况下它是关闭的。实现这样的开关可能是个好主意。

循环表示法的一个主要问题是,如果你的 Lisp 方言有一个对象系统,程序员可以创建具有自定义打印方法的新类类型,那么即使遍历这些自定义打印方法,循环表示法也必须工作。这很重要,因为在没有自定义方法的情况下,您的打印机可以在递归时轻松地将任意上下文对象传递给它自己。该对象可以指示循环符号已打开(无需继续检查动态变量,它可以为标签和诸如此类的东西保存一个哈希表)。如果打印机调用自定义打印方法,该方法可以回调打印机,则无法传递良好的内部上下文:它不是 API 的一部分。

无论如何,一个有效的简单算法是首先遍历要打印的对象,并为其所有“符合圆圈条件”的组成对象构建一个哈希表。例如,fixnum 整数或内部符号不是;不要将它们添加到哈希中。在散列(我假设这里是 Lisp 样式的散列)中,您可以将 nil 与首次引入的对象相关联。如果看到对象的副本,nil 将翻转为 t。当然,任何时候你发现访问的对象已经在散列中,你不会递归它。那将打败整个练习。构建哈希后,您可以在打印时引用它。当要打印一个“符合圆圈条件的”对象时,您首先在散列中查找它。如果哈希有一个t,那么你用标签计数器的值替换t,发出文本#<counter>=,其中<counter>是标签计数器值,然后打印对象。计数器递增。如果散列将对象与整数相关联,则意味着您之前已经为该对象打印了#<counter>= 符号。这只是对该对象的引用:因此只需打印 #<that integer># 来表示该对象,就完成了。

这是基本的想法。如果您决定使用 consing 点实现平面列表表示法,您将需要以下逻辑。例如,考虑循环列表#1=(a b c . #1#)。如果打印机没有注意到循环,它只会永远打印(a b c a b c a b c ...,或者直到达到配置的列表长度限制。它只是循环直到cdr 迭代器碰到一个原子。在循环模式下,打印机在渲染平面列表时必须观察 cdr 迭代器是否在循环哈希中命中。如果是这样,那么它必须打印 consing 点和符号,关闭括号并终止循环。 #= 的情况可以出现在这个位置:(a b c . #1=(d e . #1#))。一个积极的哈希命中基本上被视为一个终止原子。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-22
    • 2021-02-10
    • 1970-01-01
    • 1970-01-01
    • 2012-07-23
    相关资源
    最近更新 更多