Scala 领域特定语言与工具库深度解析
1. 内部与外部 DSL 的比较
在 Scala 中,内部 DSL 和外部 DSL 各有特点。以下是它们的示例代码:
- 内部 DSL:
val biweeklyDeductions = biweekly { deduct =>
deduct federal_tax (25.0 percent)
deduct state_tax (5.0 percent)
deduct insurance_premiums (500.0 dollars)
deduct retirement_savings (10.0 percent)
}
- 外部 DSL:
val input = """biweekly {
federal tax 20.0 percent,
state tax 3.0 percent,
insurance premiums 250.0 dollars,
retirement savings 15.0 percent
}"""
外部 DSL 更为简单,但需要将其嵌入字符串中,这导致代码补全、重构、颜色编码等 IDE 特性无法使用。不过,外部 DSL 实现起来更容易,且对 Scala 解析技巧的依赖较小,稳定性更高。
在选择使用哪种 DSL 时,需要根据具体情况权衡。如果 DSL 与 Scala 语法较为接近,使用内部 DSL 通常能提供更好的用户体验;如果 DSL 与 Scala 语法差异较大,如 SQL,使用带有引号字符串的外部 DSL 可能是更好的选择。
此外,我们还可以实现自己的字符串插值器,以更简单的语法封装解析器。例如,实现一个 SQL 解析器后,用户可以使用 sql"SELECT * FROM table WHERE …;" 来调用,而无需显式使用解析器 API 调用。
2. Scala 命令行工具
即使大部分工作使用 IDE 或 SBT REPL 完成,了解命令行工具的工作原理仍能提供额外的灵活性。所有 Scala 命令行工具都位于 SCALA_HOME/bin 目录下,更多信息可在 http://www.scala-lang.org/documentation/ 查看。
2.1 scalac 命令行工具
scalac 命令用于编译 Scala 源文件并生成 JVM 类文件。它是 java 命令的 shell 脚本包装器,会添加 Scala JAR 文件到类路径并定义一些 Scala 相关的系统属性。调用方式如下:
scalac <options> <source files>
源文件名不必与文件中的公共类名匹配,包声明也不必与目录结构匹配。但为了符合 JVM 要求,每个类型都会生成一个单独的类文件,类文件会写入与包声明对应的目录。
以下是 scalac 的部分常用选项:
| 选项 | 描述 |
| ---- | ---- |
| -Dproperty=value | 直接将 -Dproperty=value 传递给运行时系统 |
| -Jflag | 直接将 Java 标志传递给运行时系统 |
| -Pplugin:opt | 将选项传递给编译器插件 |
| -X | 打印高级选项的概要 |
| -bootclasspath path | 覆盖引导类文件的位置 |
| -classpath path | 指定用户类文件的查找位置 |
| -d directory or jar | 生成的类文件的目标位置 |
| -deprecation | 输出使用已弃用 API 的源位置 |
| -encoding encoding | 指定源文件使用的字符编码 |
| -explaintypes | 更详细地解释类型错误 |
| -feature | 对未显式导入的特性使用发出警告并显示源位置 |
| -g:level | 指定生成的调试信息级别 |
| -help | 打印标准选项的概要 |
| -language:feature | 启用一个或多个语言特性 |
| -nowarn | 不生成警告 |
| -optimise | 通过对程序应用优化生成更快的字节码 |
| -print | 打印去除所有 Scala 特定特性的程序 |
| -sourcepath path | 指定输入源文件的查找位置 |
从 Scala 2.10 开始,一些高级语言特性变为可选,可使用 -feature 选项对未正确导入这些特性的使用发出警告。可选语言特性列表由 scala.language 对象中的值定义,如下表所示:
| 名称 | 描述 |
| ---- | ---- |
| dynamics | 启用 Dynamic 特征 |
| postfixOps | 启用后缀运算符 |
| reflectiveCalls | 启用使用结构类型 |
| implicitConversions | 启用定义隐式方法和成员 |
| higherKinds | 启用编写高阶类型 |
| existentials | 启用编写存在类型 |
| experimental | 包含尚未在生产环境中测试的新特性,目前 Scala 中只有宏属于此类 |
高级 -X 选项可控制详细诊断输出、微调编译器行为等。部分 -X 选项如下:
| 选项 | 描述 |
| ---- | ---- |
| -Xcheckinit | 包装字段访问器,在未初始化访问时抛出异常 |
| -Xdisable-assertions | 不生成断言或假设 |
| -Xexperimental | 启用实验性扩展 |
| -Xfatal-warnings | 如果有任何警告则编译失败 |
| -Xfuture | 开启“未来”语言特性 |
| -Xlint | 启用推荐的额外警告 |
| -Xlog-implicit-conversions | 每当插入隐式转换时打印消息 |
| -Xlog-implicits | 显示更多关于某些隐式不适用的详细信息 |
| -Xmain-class path | 指定 JAR 文件清单中 Main-Class 条目的类 |
| -Xmigration:v | 对自 Scala 版本 v 以来行为可能发生变化的构造发出警告 |
| -Xscript object | 将源文件视为脚本并包装在主方法中 |
| -Y | 打印私有选项的概要 |
建议常规使用 -deprecation 、 -unchecked 、 -feature 和 -Xlint 选项,它们有助于预防一些错误并鼓励淘汰过时的库。
2.2 scala 命令行工具
scala 命令用于运行程序,若未提供程序参数,则启动 REPL。它也是一个 shell 脚本,调用方式如下:
scala <options> [<script|class|object|jar> <arguments>]
scala 接受与 scalac 相同的选项,此外还有一些额外选项:
| 选项 | 描述 |
| ---- | ---- |
| -howtorun | 指定要运行的内容,如脚本、对象、JAR 文件,或让其自动猜测(默认) |
| -i file | 在启动 REPL 前预加载文件内容 |
| -e string | 将字符串作为在 REPL 中输入的内容执行 |
| -save | 将编译后的脚本保存到 JAR 文件中,以便后续使用,避免重新编译的开销 |
| -nc | 不运行编译守护进程 fsc |
第一个非选项参数被解释为要运行的程序。若未指定任何内容,则启动 REPL。在指定程序时,程序参数后的任何参数都会传递给程序的 args 数组。
在交互式模式下,可使用 -i file 选项预加载文件。进入 REPL 后,也可使用 :load path 命令加载文件。REPL 中还有许多可用命令,部分如下:
| 命令 | 描述 |
| ---- | ---- |
| :cp path | 将 JAR 或目录添加到类路径 |
| :edit id or line | 编辑输入历史 |
| :help [command] | 打印摘要或特定命令的帮助信息 |
| :history [num] | 显示历史记录(可选 num 为要显示的命令数量) |
| :h? string | 搜索历史记录 |
| :imports [name name …] | 显示导入历史,标识名称的来源 |
| :implicits [-v] | 显示作用域内的隐式(可选 -v 为更详细的输出) |
| :javap path or class | 反汇编文件或类名 |
| :line id or line | 将行放置在历史记录末尾 |
| :load path | 解释指定文件中的行 |
| :paste [-raw] [path] | 进入粘贴模式或粘贴文件 |
| :power | 启用高级用户模式 |
| :quit | 退出解释器(或使用 Ctrl-D ) |
| :replay | 重置执行并重新执行所有先前的命令 |
| :reset | 将 REPL 重置为初始状态,忘记所有会话条目 |
| :save path | 将会话保存到文件中,以便后续重放 |
| :sh command line | 运行 shell 命令(结果隐式为 ⇒ List[String] ) |
| :settings [+ or -]options | 启用( + )/禁用( - )标志,设置编译器选项 |
| :silent | 禁用/启用结果的自动打印 |
| :type [-v] expr | 显示表达式的类型而不计算它 |
| :kind [-v] expr | 显示表达式类型的种类 |
| :warnings | 显示最近有警告的行中被抑制的警告 |
:power 启用的高级用户模式会添加一些用于查看内存数据(如抽象语法树和解释器属性)以及操作编译器的命令。
在 Windows 和类 Unix 系统上,可创建独立的 Scala 脚本,避免每次都使用 scala script-file-name 调用。以下是类 Unix 系统和 Windows 系统的示例:
类 Unix 系统示例:
#!/bin/sh
# src/main/scala/progscala2/toolslibs/secho
exec scala "$0" "$@"
!#
print("You entered: ")
args.toList foreach { s => printf("%s ", s) }
println
使用方式:
$ secho Hello World
You entered: Hello World
Windows 系统示例:
::#!
@echo off
call scala %0 %*
goto :eof
::!#
print("You entered: ")
args.toList foreach { s => printf("%s ", s) }
println
3. scala 与 scalac 的局限性
使用 scala 运行源文件和使用 scalac 编译源文件存在一些局限性。使用 scala 执行的脚本会被包装在一个匿名对象中,这意味着脚本中不能声明包。而有些有效的脚本在不使用 -Xscript object 选项的情况下无法使用 scalac 编译,因为该选项会创建与 REPL 隐式创建的相同包装器。
例如,以下脚本使用 scala 作为脚本运行正常:
// src/main/scala/progscala2/toolslibs/example.sc
case class Message(name: String)
def printMessage(msg: Message) = println(msg)
printMessage(new Message("This works fine with the REPL"))
但如果不使用 -Xscript 选项使用 scalac 编译,会出现错误:
example.sc:3: error: expected class or object definition
def printMessage(msg: Message) = println(msg)
^
example.sc:5: error: expected class or object definition
printMessage(new Message("This works fine with the REPL"))
^
two errors found
正确的编译和运行方式如下:
scalac -Xscript MessagePrinter src/main/scala/progscala2/toolslibs/example.sc
scala -classpath . MessagePrinter
4. scalap 和 javap 命令行工具
反编译器在理解 Scala 构造在 JVM 字节码中的实现方式时非常有用。 javap 随 JDK 提供,它会输出类文件在 Java 源代码中的声明形式,即使这些类文件是由 scalac 从 Scala 代码编译而来。 scalap 是 Scala 发行版提供的工具,会输出类文件在 Scala 源代码中的声明形式,但 Scala 2.11.0 和 2.11.1 版本因失误未包含 scalap ,可从指定位置下载 JAR 文件并复制到安装目录的 lib 目录,2.11.2 版本包含了 scalap 。
以 MessagePrinter.class 为例,运行 scalap -cp . MessagePrinter 的输出如下:
object MessagePrinter extends scala.AnyRef {
def this() = { /* compiled code */ }
def main(args : scala.Array[scala.Predef.String]) : scala.Unit = {
/* compiled code */
}
}
运行 javap -cp . MessagePrinter 的输出如下:
Compiled from "example.sc"
public final class MessagePrinter {
public static void main(java.lang.String[]);
}
这两个工具都有 -help 选项,用于描述它们支持的调用选项。作为练习,可尝试反编译 progscala2/toolslibs/Complex.scala 生成的类文件,使用 javap -cp target/scala-2.11/classes toolslibs.Complex 进行反编译,并思考 + 和 - 方法名的编码方式、 real 和 imaginary 字段的“getter”方法名、这些字段使用的 Java 类型以及 scalap 和 javap 的输出结果。
综上所述,Scala 的 DSL 和命令行工具为开发者提供了丰富的功能和灵活性,但在使用过程中需要注意各种工具的特点和局限性,根据具体需求选择合适的工具和方法。
Scala 领域特定语言与工具库深度解析
5. 不同工具使用场景总结
在开发过程中,需要根据具体需求合理选择 Scala 的各类工具。下面是不同工具的使用场景总结:
| 工具 | 使用场景 |
| ---- | ---- |
| 内部 DSL | 当 DSL 与 Scala 语法较为接近,能够以合理的努力和健壮性实现时,使用内部 DSL 可提供较好的用户体验,例如在测试库中 |
| 外部 DSL | 当 DSL 与 Scala 语法差异较大,如 SQL 这类广为人知的语言,使用外部 DSL 配合引号字符串是较好的选择 |
| scalac | 用于编译 Scala 源文件生成 JVM 类文件,可通过各种选项对编译过程进行控制,如指定类路径、调试信息级别等 |
| scala | 用于运行程序或启动 REPL,提供了丰富的命令用于交互式开发,可预加载文件、执行脚本等 |
| scalap | 用于查看类文件在 Scala 源代码中的声明形式,帮助理解 Scala 代码在字节码层面的实现 |
| javap | 用于查看类文件在 Java 源代码中的声明形式,即使是 Scala 代码编译的类文件也适用 |
6. 构建独立 Scala 脚本流程
在 Windows 和类 Unix 系统上构建独立 Scala 脚本的流程如下:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(选择系统类型):::process --> B{类 Unix 系统}:::process
A --> C{Windows 系统}:::process
B --> D(创建脚本文件):::process
D --> E(添加脚本内容和执行命令):::process
E --> F(设置文件可执行权限):::process
F --> G(运行脚本):::process
C --> H(创建批处理文件):::process
H --> I(添加脚本内容和调用命令):::process
I --> J(运行脚本):::process
具体操作步骤如下:
1. 类 Unix 系统 :
- 创建脚本文件,例如 secho 。
- 在文件中添加如下内容:
#!/bin/sh
# src/main/scala/progscala2/toolslibs/secho
exec scala "$0" "$@"
!#
print("You entered: ")
args.toList foreach { s => printf("%s ", s) }
println
- 设置文件可执行权限,使用命令 `chmod +x secho`。
- 运行脚本,例如 `secho Hello World`。
- Windows 系统 :
- 创建批处理文件,例如
secho.bat。 - 在文件中添加如下内容:
- 创建批处理文件,例如
::#!
@echo off
call scala %0 %*
goto :eof
::!#
print("You entered: ")
args.toList foreach { s => printf("%s ", s) }
println
- 运行脚本,例如 `secho.bat Hello World`。
7. 常用编译器选项使用建议
为了提高代码质量和开发效率,建议常规使用以下编译器选项:
- -deprecation :输出使用已弃用 API 的源位置,帮助开发者及时发现并替换过时的库和方法,避免潜在的兼容性问题。
- -unchecked :启用额外的警告,当生成的代码依赖于某些假设时会给出提示,有助于发现一些隐藏的错误。
- -feature :对未显式导入的特性使用发出警告并显示源位置,促使开发者规范使用语言特性,提高代码的可读性和可维护性。
- -Xlint :启用推荐的额外警告,虽然可能会对某些代码库产生过多警告,但能帮助开发者发现一些常见的编程错误和不良实践。
在 build.sbt 文件中使用这些选项的示例如下:
scalacOptions ++= Seq("-deprecation", "-unchecked", "-feature", "-Xlint")
8. 反编译工具实践操作
使用 scalap 和 javap 反编译工具时,可按照以下步骤进行实践操作:
1. 编译 Scala 代码,例如编译 progscala2/toolslibs/Complex.scala 文件,可使用 SBT 进行编译。
2. 使用 scalap 反编译,命令如下:
scalap -cp target/scala-2.11/classes toolslibs.Complex
该命令会输出类文件在 Scala 源代码中的声明形式。
3. 使用 javap 反编译,命令如下:
javap -cp target/scala-2.11/classes toolslibs.Complex
该命令会输出类文件在 Java 源代码中的声明形式。
在实践过程中,可以思考以下问题:
- + 和 - 方法名在字节码中是如何编码的?
- real 和 imaginary 字段的“getter”方法名是什么?
- 这些字段使用的 Java 类型是什么?
- scalap 和 javap 的输出结果有哪些差异?
通过对这些问题的思考,可以更深入地理解 Scala 代码在 JVM 中的实现方式。
9. 总结
Scala 提供了丰富的领域特定语言(DSL)和命令行工具,为开发者带来了强大的功能和高度的灵活性。内部 DSL 和外部 DSL 各有优势,在不同的场景下能发挥出不同的作用。命令行工具如 scalac 、 scala 、 scalap 和 javap 则在编译、运行、调试和反编译等方面提供了有效的支持。
然而,在使用这些工具和技术时,开发者需要充分了解它们的特点和局限性。例如,内部 DSL 虽然用户体验较好,但实现可能较为复杂;外部 DSL 实现简单,但缺乏 IDE 支持。 scala 和 scalac 在使用上也存在一些限制,需要注意脚本的编写和编译方式。
建议开发者在实际开发中,根据具体的业务需求和项目特点,合理选择和使用这些工具和技术。同时,养成使用编译器选项如 -deprecation 、 -unchecked 、 -feature 和 -Xlint 的习惯,以提高代码质量和开发效率。通过不断实践和探索,开发者能够更好地掌握 Scala 的领域特定语言和命令行工具,为项目的成功开发奠定坚实的基础。
超级会员免费看
86

被折叠的 条评论
为什么被折叠?



