如何在Scala REPL中研究对象/类型等?

43

我已经使用Scala工作了一段时间,并编写了超过10,000行的程序,但我仍然对一些内部工作方式感到困惑。我从Python转入Scala,之前已经非常熟悉Java、C和Lisp,但即使这样,进展也很缓慢,一个巨大的问题是在尝试使用Scala REPL调查对象/类型/类等内部机制时,我经常发现非常困难,而这与Python相比就很棘手。在Python中,您可以使用“foo”来研究任何对象(类型、全局变量中的对象、内置函数等等),以查看该对象的求值结果,“type(foo)”显示其类型,“dir(foo)”告诉您可以调用的方法,“help(foo)”获取内置文档。您甚至可以做一些诸如“help('re')”之类的事情,以找出名为“re”的软件包(其中包含正则表达式对象和方法)的文档,即使没有与之关联的对象。

在Scala中,您可以尝试在线阅读文档,查找库的源代码等等,但是对于您不知道它们在哪里或甚至是什么的东西来说,这通常会非常困难(而且通常需要处理大量的类型层次结构)——各种东西都漂浮在不同的地方(包“scala”、“Predef”、各种隐式转换、几乎无法搜索的符号“::”)。REPL应该是直接探索的方式,但实际上,情况要复杂得多。假设我在某个地方看到了对“foo”的引用,但我不知道它是什么。显然,不存在“使用REPL系统地调查Scala事物的指南”,但以下是我在大量试错后拼凑出来的:

  1. 如果“foo”是一个值(这可能包括存储在变量中的内容以及伴生对象和其他Scala“object”),则可以直接评估“foo”。这应该告诉您结果的类型和值。有时结果有帮助,有时没有。
  2. 如果“foo”是一个值,则可以使用“:type foo”获取其类型。(并不一定启发人心。)如果在函数调用上使用此选项,则会获取返回值的类型,而不调用该函数。
  3. 如果“foo”是一个值,则可以使用“foo.getClass”获取其类。(通常比前一个更有启发性,但对象的类与其类型有何不同?)
  4. 对于类“foo”,可以使用“classOf [foo]”,尽管结果的含义不明显。
  5. 理论上,可以使用“:javap foo”来反编译类——这应该是最有用的,但对我来说完全失败且一致。
  6. 有时您必须从错误消息中拼凑出结果。

使用“:javap”的失败示例:

scala> :javap List
Failed: Could not find class bytes for 'List'

提示错误信息的例子:

scala> assert
<console>:8: error: ambiguous reference to overloaded definition,
both method assert in object Predef of type (assertion: Boolean, message: => Any)Unit
and  method assert in object Predef of type (assertion: Boolean)Unit
match expected type ?
              assert
              ^

好的,现在让我们尝试一个简单的例子。

scala> 5
res63: Int = 5

scala> :type 5
Int

scala> 5.getClass
res64: java.lang.Class[Int] = int

足够简单...

现在,让我们尝试一些实际情况,这些情况并不那么明显:

scala> Predef
res65: type = scala.Predef$@3cd41115

scala> :type Predef
type

scala> Predef.getClass
res66: java.lang.Class[_ <: object Predef] = class scala.Predef$
这是什么意思?为什么Predef的类型仅为type,而类为scala.Predef$?我了解到 $ 是将伴生对象嵌入 Java 的方式... 但是谷歌上的 Scala 文档告诉我 Predefobject Predef extends LowPriorityImplicits -- 我如何从 REPL 推断出这一点?我该如何查看它内部的内容?
好的,让我们再试一件令人困惑的事情:
scala> `::`
res77: collection.immutable.::.type = ::

scala> :type `::`
collection.immutable.::.type

scala> `::`.getClass
res79: java.lang.Class[_ <: object scala.collection.immutable.::] = class scala.collection.immutable.$colon$colon$

scala> classOf[`::`]
<console>:8: error: type :: takes type parameters
              classOf[`::`]
                      ^

scala> classOf[`::`[Int]]
res81: java.lang.Class[::[Int]] = class scala.collection.immutable.$colon$colon

好的,这让我完全困惑了,最终我不得不去阅读源代码才能理解所有内容。

所以,我的问题是:

  1. 从真正的Scala专家那里,推荐使用REPL来理解Scala对象、类、方法等的最佳方式是什么,或者至少从REPL中尽可能地调查它们?
  2. 如何在REPL中为内置的内容使:javap工作? (默认情况下不应该工作吗?)

感谢您提供任何启示。

4个回答

33
您提到了一个重要的问题,Scala 缺乏一些文档。REPL 是一个很棒的工具,但它并不像它可以那么棒。有太多缺失的功能和可以改进的功能 - 您在帖子中提到了其中一些。Scaladoc 也是一个不错的工具,但离完美还有很长的路要走。此外,API 中的许多代码尚未或过少地记录,并且代码示例经常缺失。IDE 充满了漏洞,与 Java IDEs 显示给我们的可能性相比,它们看起来像一些幼儿园玩具。
尽管如此,与我开始学习 Scala 2-3 年前可用的工具相比,Scalas 当前工具存在巨大差异。当时,IDEs 在后台持续编译一些垃圾,编译器每隔几分钟就会崩溃,而某些文档根本不存在。经常让我感到愤怒,希望 Scala 作者死亡和腐败。
现在呢?我再也没有这些愤怒的情绪了。因为我们目前拥有的工具虽然不完美,但非常棒!

我可以为您翻译,以下是需要翻译的内容:

docs.scala-lang.org,它总结了许多优秀文档。其中包括教程、速查表、术语表、指南等等众多优秀资源。另一个好工具是Scalex,它能找到人们想到的最奇怪的操作符。它就像Scalas Hoogle,虽然它还不如它那个伟大的理想,但它非常有用。

Scala2.10带来了巨大的改进,其中包括Scalas自己的反射库:

// needs Scala2.10M4
scala> import scala.reflect.runtime.{universe => u}
import scala.reflect.runtime.{universe=>u}

scala> val t = u.typeOf[List[_]]
t: reflect.runtime.universe.Type = List[Any]

scala> t.declarations
res10: Iterable[reflect.runtime.universe.Symbol] = SynchronizedOps(constructor List, method companion, method isEmpty, method head, method tail, method ::, method :::, method reverse_:::, method mapConserve, method ++, method +:, method toList, method take, method drop, method slice, method takeRight, method splitAt, method takeWhile, method dropWhile, method span, method reverse, method stringPrefix, method toStream, method removeDuplicates)

新反射库的文档仍在缺失中,但正在进行中。它使得在 REPL 中轻松地使用 scalac 成为可能:

scala> u reify { List(1,2,3) map (_+1) }
res14: reflect.runtime.universe.Expr[List[Int]] = Expr[List[Int]](immutable.this.List.apply(1, 2, 3).map(((x$1) => x$1.$plus(1)))(immutable.this.List.canBuildFrom))

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> import scala.reflect.runtime.{currentMirror => m}
import scala.reflect.runtime.{currentMirror=>m}

scala> val tb = m.mkToolBox()
tb: scala.tools.reflect.ToolBox[reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@32f7fa37

scala> tb.parseExpr("List(1,2,3) map (_+1)")
res16: tb.u.Tree = List(1, 2, 3).map(((x$1) => x$1.$plus(1)))

scala> tb.runExpr(res16)
res18: Any = List(2, 3, 4)

当我们想要了解Scala代码的内部转换时,使用以下命令scala -Xprint:typer -e "List(1,2,3) map (_+1)"可以得到内部表示,这将变得更加方便。此外,在新版本中还发现了一些小的改进,例如:
scala> :type Predef
scala.Predef.type

Scaladoc将增加一些类型层次结构图(单击类型层次结构)。

现在使用宏,可以以极大的方式改进错误消息。有一个名为expecty的库可以实现此功能:

// copied from GitHub page
import org.expecty.Expecty

case class Person(name: String = "Fred", age: Int = 42) {
  def say(words: String*) = words.mkString(" ")
}

val person = Person()
val expect = new Expecty()

// Passing expectations

expect {
  person.name == "Fred"
  person.age * 2 == 84
  person.say("Hi", "from", "Expecty!") == "Hi from Expecty!"
}

// Failing expectation

val word1 = "ping"
val word2 = "pong"

expect {
  person.say(word1, word2) == "pong pong"
}

/*
Output:

java.lang.AssertionError:

person.say(word1, word2) == "pong pong"
|      |   |      |      |
|      |   ping   pong   false
|      ping pong
Person(Fred,42)
*/

有一个工具可以帮助你在GitHub上找到托管的库,叫做ls.implicit.ly

现在的IDE已经具备了一些语义高亮的功能,用于显示成员是对象/类型/方法/其他什么的。ScalaIDE的语义高亮功能。

REPL的javap功能只是调用本地的javap,因此它不是一个非常丰富的工具。您必须完全限定模块的名称:

scala> :javap scala.collection.immutable.List
Compiled from "List.scala"
public abstract class scala.collection.immutable.List extends scala.collection.AbstractSeq implements scala.collection.immutable.LinearSeq,scala.Product,scala.collection.LinearSeqOptimized{
...

不久前,我写了一篇介绍Scala代码如何编译成字节码的概述,其中包含很多需要了解的内容。

最好的是:这些都是在过去几个月内完成的!

那么,在REPL中如何使用所有这些东西呢?嗯,目前还不可能...但有一天我们会拥有这样的REPL。一个可以在我们想要查看文档时显示文档的REPL。一个可以让我们与其通信的REPL(也许像lambdabot一样)。一个让我们做我们仍然无法想象的酷炫事情的REPL。我不知道这将在什么时候实现,但我知道在过去的几年里已经完成了很多工作,而在未来的几年中将完成更大的工作。


1
@DanielC.Sobral:文档在过去几个月中有了很大的改进,这是真的。然而,我觉得它并不好 - 我觉得它只是足够开始使用。许多高阶方法的文档不足(例如mapfoldLeft)- 它们没有提供如何使用它们的示例。对于缺乏高阶函数经验的初学者来说,这是至关重要的。这就是为什么我写“有点缺乏”而不是“完全缺乏”的原因。 - kiritsuku
2
我肯定很想了解你的答案更新以及过去三年中事情的变化。 - Lucian Enache
我非常希望您的回答能够更新一下,因为我仍然面临着与 OP 相同的许多问题,而现在已经过去了将近4年。肯定情况会有所改善吧? - rkrzr
@rkrzr 注意到了,但我现在没有动力写这个。我能说的是,文档仍然是一个问题,并没有像我希望的那样成熟。今天有其他可能性,但要写下来太多工作了... - kiritsuku
太糟糕了。Scalex已经被废弃,在GitHub上标记为此状态,并且域名已经恢复为Yahoo。 - Bob Kerns
显示剩余6条评论

5
Javap可以工作,但你将其指向了scala.Predef.List,这是一个type,而不是一个class。请改为指向scala.collection.immutable.List
现在,大多数情况下,只需输入一个值并查看结果的类型就足够了。有时使用:type可能会有所帮助。我发现使用getClass是一个非常糟糕的方法。
此外,有时你会混淆类型和值。例如,在这里,你引用了对象::
scala> `::`.getClass res79: java.lang.Class[_ <: object
scala.collection.immutable.::] = class
scala.collection.immutable.$colon$colon$

这里你引用了类 :::

scala> classOf[`::`[Int]] res81: java.lang.Class[::[Int]] = class
scala.collection.immutable.$colon$colon

对象和类不是同一件事情,事实上,有一个常见的模式是对象和类使用相同的名称,并具有特定名称以描述它们之间的关系:伴生对象。
不要使用dir,只需使用制表符完成。
scala> "abc".
+                     asInstanceOf          charAt                codePointAt           codePointBefore       codePointCount
compareTo             compareToIgnoreCase   concat                contains              contentEquals         endsWith
equalsIgnoreCase      getBytes              getChars              indexOf               intern                isEmpty
isInstanceOf          lastIndexOf           length                matches               offsetByCodePoints    regionMatches
replace               replaceAll            replaceFirst          split                 startsWith            subSequence
substring             toCharArray           toLowerCase           toString              toUpperCase           trim

scala> "abc".compareTo
compareTo             compareToIgnoreCase

scala> "abc".compareTo
                             def compareTo(String): Int

如果您进入电源模式,您将获得更多信息,但这对初学者来说几乎是不可能的。上面展示了类型、方法和方法签名。Javap将反编译代码,但这需要您对字节码有很好的掌握。
其中还有其他内容--一定要查找“:help”,看看有什么可用的。
文档仅通过scaladoc API提供。将其保持在浏览器中打开,并使用其搜索功能快速查找类和方法。另外,请注意,与Java相反,您不需要通过继承列表导航以获取方法的描述。
它们也可以完美地搜索符号。我怀疑您没有花太多时间在scaladoc上,因为其他文档工具就不如它。例如,Javadoc--浏览包和类非常糟糕。
如果您有特定的问题,像Stack Overflow那样,使用Symbol Hound来搜索符号。
使用夜间版 Scaladocs:它们将与您正在使用的任何版本不同,但它们始终是最完整的。此外,现在它们在许多方面都要好得多:您可以使用TAB在框架之间切换,在搜索框上自动聚焦,您可以使用箭头在过滤后导航到左侧框架,并使用ENTER使所选元素出现在右侧框架中。它们具有隐式方法列表,并且具有类图。我已经用一个远不如此强大的REPL和一个远不如此贫乏的Scaladoc来完成工作了。当然,我跳到了trunk(现在的HEAD)只是为了获得tab-completion。

谢谢,我之前不知道Tab自动完成或者Power Mode,还有不同的列表。你的信息也强调了我之前说的,没有明显的文档告诉我这些事情。我经常使用Scaladoc,但是在我看来,它并不像REPL那样理想,因为REPL可以让你直接查看当前Scala中实际存在的内容,而不是其他的。至于::,我知道它既有类型又有类,但是问题在于当你不是Scala专家时,往往不清楚哪个是哪个,而且在使用REPL时不需要提前知道这一点。 - Urban Vagabond

4
请注意,Scala 2.11.8 Scala REPL中的新标签补全功能可以促进类型探索/发现。它现在包括:
  • 驼峰式自动补全:
    尝试:
    (l: List[Int]).rroTAB
    它会扩展为:
    (l: List[Int]).reduceRightOption

  • 通过键入名称中的任何驼峰式部分来查找成员:
    尝试:
    classOf[String].typTAB, 获取 getAnnotationsByTypegetComponentType和其他内容

  • 不需要键入get即可完成bean的getter方法:
    尝试:
    (d: java.util.Date).dayTAB

  • 按两次TAB键可查看方法签名:
    尝试:
    List(1,2,3).partTAB
    它会自动扩展为:
    List(1,2,3).partition;
    再次按下TAB以显示:
    def partition(p: Int => Boolean): (List[Int], List[Int])


2
你需要传递完全限定类名给 javap
首先使用 classOf 获取它:
scala> classOf[List[_]]
res2: java.lang.Class[List[_]] = class scala.collection.immutable.List

然后使用 javap(对我来说无法从repl中工作:":javap在此平台上不可用")所以示例是从命令行中,我相信,在repl中,您不需要指定类路径:

d:\bin\scala\scala-2.9.1-1\lib>javap -classpath scala-library.jar "scala.collection.immutable.List"

但我怀疑这不会对你有帮助。可能你正在尝试使用你在动态语言中曾经使用的技巧。我极少在scala中使用repl(而在javascript中经常使用它)。IDE和源代码是我的全部。


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