1. 命令行任务

我们的Scala 3之旅,始于命令行。比如,在你按照“安装Scala”操作后,你可能会想要从操作系统的命令行中输入 scala 来启动REPL——Scala的 Read/Eval/Print/Loop (添加备注:REPL基本上是现在所有主流语言的标配 java也有javashell了)。或者你可能想要创建一个小小的基于文件的“Hello, world“项目,然后来编译运行它。因为许多人开始使用Scala是从命令行任务开始的,所以我们先从这里开始介绍。

REPL是一个命令行 shell,它是一个小实验室,你可以在这里运行各种小测试来看看Scala和它的一些第三方库是如何工作的。如果你熟悉Java的JShell,Ruby的 irb,Python的shell或者IPython,或者Haskell的 ghci,那么Scala的REPL与这些都很相似。如图1-1所示,只要在操作系统命令行输入 scala 就可以启动REPL,然后再输入Scala表达式然后就会在 shell 中被求值。

当你想测试一些Scala代码时,REPL是一个绝佳的实验环境。没必要创建一个完整的工程——只需要将你的测试代码放到REPL中进行实验,直到你知道它能工作了为止。由于REPL是一个非常重要的工具,它最重要的功能将在本章的前两个例子中演示。

f1-1

图1-1. macOS 终端窗口中运行Scala3 REPL

虽然REPL非常棒,但它不是你唯一的选项。Ammonite REPL 最初是为Scala 2创建的,它比Scala2 REPL有更多的功能,包括:

  • 能从GitHub和Maven仓库中导入代码

  • 保存和恢复会话的能力

  • 美化打印输出

  • 多行编辑

在写这本书的时候, Ammonite仍在为Scala 3做迁移, 但许多重要的功能已经可以使用了。关于如何使用这些功能的例子,见1.3节。

最后,当你需要构建Scala项目时,你通常会使用像sbt这样的构建工具,这将在第17章中演示。但如果你想编译和运行一个小型的Scala应用,比如只有一两个文件,你可以用 scalac 命令编译你的代码,然后使用 scala 运行就像在Java中使用 javacjava 命令那样。这些过程将在1.4节中进行演示。之后,1.6节展示了如何使用 javascala 命令运行你打包成JAR文件的应用程序。

1.1 Scala REPL 入门

问题

你想要开始使用Scala REPL并了解它的一些基本功能。

解决方案

如果你使用过Java、Python、Ruby和Haskell等语言的REPL环境,你会觉得Scala REPL很熟悉。在操作系统的命令行中输入 scala来启动REPL。当REPL启动时,可能看到一个初始信息,紧接着的是 scala >提示符:

该提示表明你正在使用Scala REPL。在REPL环境中,你可以尝试各种不同的实验和表达式:

正如上述例子所示:

  • 输入命令后,REPL输出显示你的表达式的结果,包括数据类型信息。

  • 如果没有指定一个变量名,如第三个例子所示,REPL会创建自己的变量,以res0开头然后是res1以此类推。你可以直接使用这些变量名,就像这些变量是你创建的那样:

初学者和有经验的开发者每天都会在REPL中编写代码,以快速了解Scala的功能和他们自己的算法是如何工作的。

Tab 补全

有一些简单的技巧可以使REPL的使用更加有效。一个技巧是使用tab 补全来查看一个对象上的可用方法。我们可以通过输入1然后一个小数点再然后按Tab键来看看tab 补全是如何工作的。REPL的响应是列出Int实例上的几十个可用方法:

你也可以通过输入方法名的第一部分,然后按Tab键来限制所显示的方法列表。比如,如果你想看List上所有的方法,输入List(1)。然后按Tab键,你会看到超过200个方法。但如果你只关心List上以to开头的方法,那么你可以输入List(1).to 然后按Tab键,输出结果就会减少到只剩下这些方法:

讨论

我使用REPL创建了许多小实验,这有助于理解Scala自动执行的一些类型转换。比如,当我刚开始使用Scala时,在REPL中输入以下代码时,我不知道变量 x 的类型是什么:

在REPL中很容易运行这样的测试,然后在一个变量上调用getClass来查看其类型:

虽然对于刚开始使用Scala的你来说,上面这一行输出的可读性有些差,但是=右边的信息能让你知道这个类型是一个 Tuple3

你也可以使用REPL的 :type 命令来查看类似的信息,尽管并不会显示 Tuple3 的名字:

然而,一般来说,在许多其他情况下,这个命令还是有帮助的:

虽然这些都是简单的例子,但你会发现,当处理更复杂的代码以及那些你不熟悉的库时,REPL是非常有帮助的。

在sbt内部启动REPL -- TODO 耗子图

你也可以从sbt shell中启动Scala REPL会话,如17.5节“了解其他sbt命令”所示,只要在一个sbt项目中启动sbt shell:

然后在那使用 console 或者 consoleQuick 命令:

console命令会编译项目中的源文件,将其放到classpath中并启动REPL。而consoleQuick命令则会直接使用项目依赖在classpath上启动REPL(不会编译项目源代码)。consoleQuick适用于你的项目源代码不能被编译或者你只是想使用依赖库尝试一些测试代码的情况。

另见

如果你很喜欢REPL环境的想法,但又想尝试一些默认REPL以外的REPL,可以考虑下面的一些免费的REPL:

  • 在1.3节中有演示的Ammonite REPL有着比默认REPL更多的功能。

  • Scastie( https://scastie.scala-lang.org )是一个基于Web支持sbt选项并且可以添加外部库的REPL。

  • ScalaFiddle( https://scalafiddle.io )另一个基于Web的REPL。

  • IntelliJ IDEA和Visual Studio Code(VS Code) IDEs都有类似REPL的功能的worksheets。

1.2 将源代码和JAR文件加载到REPL中

问题

你想在REPL中使用源文件中的Scala代码。

解决方案

使用 :load 命令可以将源代码文件加载到REPL环境中。例如,在一个 models 的子目录里有一个名为Person.scala的文件,里面的代码如下:

你可以像下面这样将代码加载到REPL环境中:

在代码被加载到REPL后,可以创建一个新的Person实例

注意,如果你的代码中有包声明:

:load 命令将会失败

源代码文件不能在REPL环境中使用包,对于这种情况,需要将其编译成JAR包,然后放到REPL所启动工程的classpath里,例如,我是这样在REPL中使用0.2.0版的Simple Test库( https://oreil.ly/LcxCG )的:

在写这篇文章时,还不能将JAR添加到已运行的REPL会话中,但将来可能会加入这个功能

讨论

另外一个最好掌握知识点的是REPL会自动加载当前目录下已经编译好的class文件。比如,如果你将下面代码放在一个名为Cat.scala的文件中,并使用scalac进行编译,(编译器)会创建一个Cat.class文件:

如果在这个class文件所在的目录下启动REPL,就可以直接创建一个Cat

scala> Cat("Morris")

val res0: Cat = Cat(Morris)

可以使用这种技术在Unix系统下自定义REPL环境。而要做到这一点,参照以下步骤:

  1. 在home目录下创建一个文件夹名称为 repl。比如,在我的机器上创建的目录是 /User/al/repl。(你可以用自己喜欢的名字来命名)

  2. 把任何你想要的 *.class 文件放到该目录下。

  3. 创建一个alias或者shell脚本以便用来启动该目录下的REPL。

在我的系统中,将 Repl.scala 放在了 ~/repl 中,它的代码如下:

然后使用 scalac 编译,在这个目录下创建其class文件。并用下面的方式创建并使用alias来启动REPL:

这个alias将当前工作目录设置成~/repl,然后启动REPL,最后在退出REPL时返回到之前工作的目录。

另一种方法,可以创建一个名为 repl 的脚本并使其可执行,然后放到 ~/bin 目录里(或者 PATH 里包含的其他地方):

因为shell脚本是在一个子进程中运行的,所以当你退出REPL时,你会被返回到原来的目录。

通过使用这种方法,当REPL启动时,你的自定义方法将被加载,所以你可以在scala shell中使用它们:

使用这种技术来预加载其他任何你想要在REPL中使用的自定义方法。

1.3 开始使用Ammonite REPL

问题

你想要开始使用Ammonite REPL并了解它的一些基本功能。

解决方案

Ammonite REPL的工作方式与Scala REPL一样:只要下载安装然后使用amm命令启动即可。与Scala REPL一样,Ammonite REPL会对Scala表达式求值,并自动给未分配变量名的变量分配变量名:

但Ammonite有许多额外的功能。你可以用以下命令改变shell提示符:

如果把这些Scala表达式放在foo文件夹的Repl.scala中:

可以使用以下命令将其导入到Ammonite REPL中:

然后就可以在Ammonite中使用这些方法了:

类似地,可以使用Ammonite的 $cp 变量将文件夹 foo 中的JAR文件 simpletest_3.0.0-0.2.0.jar导入到amm REPL会话中:

通过import ivy命令,可以从Maven Central(或者其他资源库)导入依赖,并在当前的shell中使用:

Ammonite的内置 time 命令可以计算运行代码所需的时间:

Ammonite的自动补全能力令人印象深刻。只要像下面这样输入表达式,然后在小数点后按Tab键:

@ Seq("a").map(x => x.

这么做时,Ammonite会显示一长串对于字符串x可用的方法:

因为不仅显示了方法名称,还显示了它们的输入参数和返回类型,这对于我们是很有帮助的。

讨论

Ammonite还有很多其他的功能,另一个很有用的功能是,可以像Unix的.bashrc或者.bash_profile一样为其提供一个启动配置文件。只要像下面这样将表达式放在 ~/.ammonite/predef.sc 文件中:

当启动Ammonite REPL时,提示符将被替换成 yo:,而且定义的其他方法也是可用的。

还有另一个很有用的功能是可以保存REPL会话,这将保存之前所有的操作。可以通过下面一个测试,即在REPL中创建一个变量,然后将其保存到会话中:

然后创建另一个变量:

现在重新加载会话,会看到向期望的一样remember变量仍然可用而forget变量已经不可用了:

也可以像下面这样通过给会话取名的方式来保存或者恢复多个会话:

关于更多的功能细节,请参见Ammonite文档( https://ammonite.io/#Ammonite-REPL

1.4 使用scalac编译,使用scala运行

问题

虽然你通常会使用像sbt或Mill这样的构建工具来构建Scala应用程序,但偶尔你可能会想使用更基本的工具来编译和运行小型测试程序,就像你可能会使用javac和java来处理小型Java应用程序一样。

解决方案

用scalac编译小程序,然后用scala运行它们。例如,有以下名为Hello.scala的源码:

使用 scalac 在命令行中编译:

然后用scala运行它,给 scala 命令传递一个用 @main 标记的方法名:

讨论

编译和运行类与Java是一样的,包括像classpath这样的概念。例如,假设有一个Pizza.scala的文件中有一个 Pizza 类,并且它依赖一个 Topping 类型:

假设 Topping 的定义是这样:

并且它在Topping.scala的文件中,并且已经在一个名为classes的子目录中被编译成Topping.class,可以这样编译Pizza.scala

$ scalac -classpath classes Pizza.scala

注意,scalac命令有很多额外的选项可以使用。例如,如果你在前面的命令中加上-verbose选项,你会看到数百行额外的输出用来显示scalac是如何工作的。这些选项可能会随着时间的推移而改变,所以可以使用-help选项来查看额外的信息:

Main 方法

在我们谈论编译 main 方法的时候,了解Scala3可以用两种方式来声明它是很有帮助的:

  • 在一个方法上使用 @main 注解

  • 在一个object中声明一个签名正确的 main 方法

像前面所示,一个简单的不需要参数的 @main 方法可以这样声明:

也可以声明一个 @main 方法来接收命令行传入的参数,比如在这个例子中接收一个 StringInt

在使用scalac编译这段代码后,可以这样来运行:

$ scala hello "Lori" 44

Hello, Lori, I think you are 44 years old.

对于第二种方法,在object内部声明main方法就像在Java中声明main方法一样,而Scala中的main方法的签名必须是这样:

这段Scala代码与下面的Java代码类似:

1.5 反编译Scala代码

问题

在学习Scala代码如何编译成class文件的过程中,或者试图理解某个特定的问题时,你可能会想要检查由Scala编译器从源代码生成的字节码。

解决方案

反编译Scala代码的主要方法是使用javap命令,也可以使用反编译器将class文件转换回Java源代码,这种方式将会在后面讨论。

使用javap

因为Scala源代码被编译成了普通的JVM类文件,所以可以使用javap来反编译它们。比如,假设有一个Person.scala的文件,它包含的代码如下所示:

下一步,使用scalac编译该文件:

现在可以像这样使用 javap 将生成的 Person.class 反编译生成其函数签名:

以上代码显示了 Person 类的公有签名,也就是它的公共API或者说接口。即使是这么简单一个例子,也可以看出Scala编译器做的工作,创建了 name(),name_$eq, age(),和 age_$eq等方法。在下面的讨论中会有更详细的例子。

可以通过使用 javap -private选项来查看额外的信息:

javap还有几个有用的选项,使用 -c 选项可以看到组成Java字节码的实际命令,再加上 -verbose 选项可以看到更多细节。运行 javap -help 以了解所有选项的细节。

讨论

了解Scala工作方式的其中一种有效方法是,用 javap 反编译class文件获取相关信息。正如第一个例子里的Person类,将构造函数的参数 nameage 定义为 var 字段,可以生成很多方法。

作为第二个例子,把这两个字段的 var 属性去掉将得到以下定义:

scalac 编译这个类,然后在生成的class文件上运行 javap ,会得到一个更短的类签名:

相反,在两个字段上都留下 var ,并把这个类变成一个样例类,那么Scala会大大增加所生成的代码量。要想达到这个目的,只要像下面这样修改 Person.scala 就可以得到这样一个样例类:

编译这段代码会创建两个输出文件, Person.classPerson$.class 使用 javap 来反编译这两个文件会得到:

如前面所示,当把一个类定义成样例类时,Scala会为你生成量代码,上面的输出显示了这些代码的公共签名。关于这段代码的详细讨论见5.14节“用样例类生成模版代码”。

关于.tasty文件 -- 乌鸦.png TODO

细心的你可能已经注意到了,除了 .class 文件之外,Scala3在编译过程中还会生成 .tasty 文件。这些文件是以 TASTy 格式生成的,而TASTy这个缩写来自于类型化抽象语法树这个术语。

TASTy检查文档( https://docs.scala-lang.org/scala3/reference/metaprogramming/tasty-inspect.html )指出:“TASTy文件包含一个类的完整类型树,包括源代码和文档。这对于分析或重代码中提取语义信息的工具来说是非常理想的。”

它们的用途之一是用于 Scala3和Scala 2.13+之间的集成。正如“Scala向前兼容”( https://oreil.ly/P4joi )所说,“Scala2.13可以读取这些(TASTy)文件来了解哪些术语,类型和implicits是在哪个依赖中定义的,以及需要生成哪些代码来正确使用它。编译器中管理这部分的功能被称为Tasty Reader”。

另见

  • 在我的“How to Create Inline Methods in Scala 3” ( https://alvinalexander.com/scala/scala-3-inline-methods-functions-how-to/ )这篇博客中,我展示了如何使用这种技术来理解inline方法。

  • 也可以使用反编译器将 .class 文件反编译成 Java 代码。我偶尔使用一个名为JAD的工具,虽然它在2001年就停止更新了。但令人惊讶的是,二十年后它至少还能对class文件进行部分反编译。在Scala Gitter 频道( https://oreil.ly/4arCQ )中也提到一个名为CFR( https://github.com/leibnitz27/cfr )的更现代的反编译器。

更多关于TASTy和 .tasty 文件的资料如下:

  • “Macros: the Plan for Scala 3”( https://www.scala-lang.org/blog/2018/04/30/in-a-nutshell.html

  • “Forward Compatibility for the Scala 3 Transition”( https://www.scala-lang.org/blog/2020/11/19/scala-3-forward-compat.html

  • “Scala 3 Migration Guide: Compatibility Reference”( https://oreil.ly/7AXv8

1.6 使用Scala和Java运行JAR文件

问题

你想使用scalajava命令来运行一个从Scala应用程序创建的JAR文件。

解决方案

首先,像在17.1节“为sbt创建一个工程目录结构”演示的那样,创建一个最基础的sbt工程。然后在 project/plugins.sbt 文件中加入下面这行来将sbt-assembly( https://oreil.ly/Q6CUr )加入到工程配置中:

然后将下面的 Hello.scala 源代码文件放到工程的根目录:

下一步,在sbt shell中使用 assembly 或者 show assembly 来创建一个JAR文件:

如上所示,show assembly 打印了生成JAR文件的位置。这个文件以 RunJarFile 开头,是因为我创建的 build.sbt文件中 name 字段就是 RunJarFile 。同样,文件名中的 0.1.0 部分来自于该文件的 version 字段:

接下来,创建一个 Example 的子目录,并将JAR文件复制到该目录:

因为sbt-assembly会将所需要的所有东西全打包到JAR文件中,所以可以使用下面的 scala 命令来运行 hello 的main方法:

注意,如果JAR包中存在多个 @main 方法,可以用在命令的末尾制定方法的完整路径的方式来运行它们:

讨论

如果试图(a)使用 java 命令来运行JAR文件,(b)用 sbt package 而不是 sbt assembly 来打包JAR文件,就需要手动将JAR文件的依赖关系添加到classpath中。比如,当使用 java 命令运行一个JAR文件时, 需要像这样:

注意整个 java 命令应该在一行,包括行末的 foo.bar.Hello 部分。

使用这种方式,你需要找到 scala-library.jar 。在我的例子中,因为Scala 3发行版是手动管理的,所以它被放在了所示的目录中。如果使用像Coursier( https://get-coursier.io/docs/cache )这样的工具来管理Scala。那么它下载的文件可以在下面的目录里找到:

  • macOS: ~/Library/Caches/Coursier/v1

  • Linux: ~/.cache/coursier/v1

  • Windows: %LOCALAPPDATA%\Coursier\Cache\v1 假设对于用户Alvin 通常这个路径对应的是:C:\Users\Alvin\AppData\Local\Coursier\Cache\v1

有关这些目录的最新细节,请参阅Coursier Cache page( https://oreil.ly/Rs4dV )。

为什么要使用sbt-assembly?

注意,如果应用程序使用了托管和非托管依赖并且使用 sbt package 而不是 sbt assembly 就必须了解并找到所有有依赖关系以及有传递依赖关系的JAR文件,并将其包含在classpath中。这就是为什么强烈建议使用 sbt assmbly 或类似的工具来打包的原因。

另见

  • 参阅17.11节,“部署一个可执行的JAR文件”以了解更多关于如何配置和使用sbt-assembly的细节。

最后更新于

这有帮助吗?