Scala 3 的下载方式,见:Scala 3 (epfl.ch)
Scala 3 的编译器代号为 Dotty。总体而言,Scala 3 的改动可概括如下:
- 支持无括号风格的代码编写 。
- 对类型系统做了极大丰富。
- 改进了 Scala 2 中
implicit
泛滥的问题。
运行 Scala 3 需要至少 JDK 8 及以上版本的支持。另实测 2018 版本的 IntelliJ IDEA 不识别 Scala 3 的 SDK ,因此笔者将 IDE 及其插件升级到了 2020 版本。
这里有保留性地介绍了 Scala 3 版本中主要的新特性。完整的预览见官方文档: Scala 3 Syntax Summary | Scala 3 Language Reference。有关于 Scala 3 的中文资料比较缺乏,为了避免歧义,部分标题后面附上了官网的英文名词。
Scala 3 的各种语法增强都是基于 Scala 2 的,因此用户需要对 Scala 自身的各种语法有基本的了解。另外,在下文中,通过 def
定义的称之方法 Method,通过 val / var
定义的表达式,称之函数 Function。
语法改进
无括号缩进语法 Optional Braces
Scala 3 额外引入了无括号缩进语法,代码中的 {}
理论上可全部省略,用户现在可以基于两种写作风格来编写 Scala 代码。
// scala 2 风格的代码:
object Obj {
def f() : Unit = {
println("Welcome to scala 3.")
}
}
// scala 3 风格的代码:
object Obj :
def f() : Unit =
println("Welcome to scala 3.")
end f
end Obj
一旦选择无括号写法,那么就要格外注意每行代码的缩进。end XXX
并不是必须的,因此 Scala 3 的代码还可以排版为如下格式:
object Obj01 :
def f01() : Unit =
println("Welcome to scala 3.")
def f02() : Unit =
println("Welcome to scala 3.")
object Obj02 :
def g01() : Unit =
println("Welcome to scala 3.")
def g02() : Unit =
println("Welcome to scala 3.")
控制语句也可以改写成无括号形式,比如在 Scala 3 中,if
语句块,for
语句块可以写成如下形式:
// if block in scala 2
if() {} else {}
// if block in scala 3
if ... then ... else ... end if
// for block in scala 2
for(){}
// for block in scala 3
for ... do ... end for
Scala 3 给出了有关于 " spaces vs Tabs" 的使用建议,见:Optional Braces | Scala 3 Language Reference | Scala Documentation (scala-lang.org)。一个好的编程习惯是:不要在同一份源文件中混着使用空格和缩进两种方式。
可变参数拼接
在 Scala 2 中,如果要将一个数组或序列 "拆解" 并传入方法的可变参数列表中,需要表达为:
val xs: Array[Int] = Array[Int](1,2,3,4,5)
def f(xs : Int*): Unit = xs.foreach(println(_))
f(xs : _*)
Scala 3 中允许使用更简洁的表达方式 ( 类似 Groovy 中的 *xs
):
f(xs*)
另一方面,对于模式匹配的绑定符号 @ _*
:
val xs : Array[Int] = Array[Int](1,2,3,4,5)
xs match {
case Array(_,_,others @ _*) => others.foreach(println(_))
}
现在也可以使用一个简洁的 *
符号替代:
val xs : Array[Int] = Array[Int](1,2,3,4,5)
xs match {
case Array(_,_,others*) => others.foreach(println(_))
}
全局 apply 方法
apply 方法,被 Scala 称之为构造器代理 Constructor proxies ,实现 apply
方法的类在初始化时可以省略 new
关键字。在 Scala 2 中,编译器自动为被标记为样例类的 case class
提供 apply
方法。
class Person(var age : Int,var name :String)
// 省略 new 关键字背后的本质上是编译器编译并构造了一个同名的工厂方法
val p2 = Person(19,"Jane")
而在 Scala 3 中,Dotty 编译器会为所有类提供 apply
方法,因此 new
关键字就显得不那么重要了。但在某些情况下 new
关键字不可省略,比如创建一个动态混入特质的对象时:
val o = new aClass with aTrait with bTrait
infix 规范中缀表达式
早在 Scala 2 中就已经实验性地引入了中缀,前缀,后缀表达式的概念。见:Scala 分支结构与自定义运算符 - 掘金 (juejin.cn)。在 Scala 3 中,那些由字母,数字组合命名的方法,如果要将它们视作中缀表达式,建议加上 infix
关键字。
case class Box(var v : Int){
// symbolic operators 不需要显示加上 infix 关键字。
def +(that : Box) : Box = Box(this.v + that.v)
// alphanumeric operator 建议加上 infix 关键字。
infix def sub (that : Box) : Box = Box(this.v - that.v)
}
不规范的中缀符使用法现在 可能会 被编译警告不规范 ( depredated ) 。被标记为中缀符号的函数只能拥有一个参数。
除此之外,infix
还可以用在类型定义上。比如:
infix type to[X,Y] = (X, Y)
val e : Int to String = 1 -> "1"
@main 注解
Scala 3 引入了新的 @main
注解,它支持用户将任意一个单例对象的方法标记为程序入口。
object Obj:
@main
def test(): Unit = ???
另一方面,程序入口的参数列表可以根据实际内容进行精确设置,而不是将所有输入通通绑定在 args : Array[String]
上。
// 当出现不规范的输入时,程序会提示:
// Illegal command line after XXX arguments: java.lang.NumberFormatException: For input string: xxx
@main
def test(a : Int, b : Int, c : Int): Unit = println(a + b + c)
将方法赋值给函数
Scala 3 中允许直接将 def
定义的方法赋值给一个函数表达式,官方对此称之 η 拓展 ( Eta Expandition ),该名词源自于 Lambda 演算,见:什么是 eta-expansion? - 知乎。
def aMethod(a :Int , b : Int) : Int = a + b
// scala 3
val aFunction: (Int, Int) => Int = aMethod
而在 Scala 2 中,想要实现此功能需要额外携带一个 _
符号,表示为:
// scala 2
val aFunction: (Int, Int) => Int = aMethod _
改进的顶级声明
Scala 3 现在支持顶级定义变量和函数,这意味着它们是 "可导出" 的。因此, Scala 3 不再推荐用 Scala 2 的包变量定义全局内容,官网曾透露包变量的概念在不久的将来会被标记过时且被删除:Dropped: Package Objects | Scala 3 Language Reference | Scala Documentation
package myTestForScala3
// 这类声明都属于顶级声明 top-level。
var a = 1
var b = 2
def f() : Unit = ()
object Test :
@main
def test() =
g(1)(2)("3")
g(1)(2)(3)
end Test
改进的 import 写法
Scala 3 现在额外支持在引入包时使用 *
符号来表示 "通配",与 Java 的写法保持了统一。
// Scala 2:
import java.lang._
// Scala 3:
import java.lang.*
这样在表达 "引入除某个组件之外的所有内容" 时语义更清晰:
// Scala 2:
import java.lang.{Integer => _ , _ }
// Scala 3:
import java.lang.{Integer as _ , *}
另外,当引入重命名时,Scala 推荐使用 as
来替代之前的 =>
。
// Scala 2:
import java => J
// Scala 3:
import java as J
不透明类型别称 Opaque
不透明类型别称通过额外的 opaque
修饰,这相当于对外构造了一个新的抽象类型 ( abstract type ) 。
opaque type Logrithm : Double
外界无法观测到 opaque
类型所引用的真实类型,下面举个例子说明:
object MathUtil :
// Logarithm 是 Double 的一个抽象,是不透明的。
// Logarithm(x) = log(x) (底数为e)
opaque type Logarithm = Double
object Logarithm :
def apply(d : Double) : Logarithm = math.log(d)
// 只有在 Logarithm 所在定义域 MathUtil 的内部,
// Logarithm 才能被视作 Double 类型计算。
extension (ths : Logarithm)
// logM + logN = log(MN)
// 不定义 + 操作符,那么 Logarithm 在外部就不能进行 "+" 加法。
def + (that: Logarithm): Logarithm = ths + that
object B :
import MathUtil.*
// 提示 Double 和 Logarithm 不兼容。
// Double 类型的所有操作都不适用于 Logarithm。
// val v0 : Logarithm = 1.00d
val v1 : Logarithm = Logarithm(math.E)
val v2 : Logarithm = Logarithm(math.pow(math.E,3))
@main
def test11() : Unit =
// log(e) + log(e^3) = log(e^4) = 4
println(v1 + v2)
只有在 MyUtil
内部,Logarithm
才被视作为 Double
的类型别名。在另一个类 B
中虽然引入了 MythUtil.*
,但不能使用 Double
值为 v0
赋值,因为对外界而言 Logarithm
是一个与 Double
无关的类型。
柯里化函数重载
Changes in Overload Resolution | Scala 3 Language Reference | Scala Documentation (scala-lang.org)
Scala 3 现在支持柯里化函数的重载,下面举一个例子:
// 在 scala 2 会报语法错误:
def g(a : Int)(b : Int)(c : String) : Int = 0
def g(a : Int)(b : Int)(c : Int) : Int = 0
g(1)(2)("3")
g(1)(2)(3)
特别注意,Scala 3 要求重载函数作顶级声明 ( top-level )。
特质参数
特质现在可以携带参数列表,被 var
修饰的参数会被视作特质的属性。
// ip 被视作为 TCP 的一个属性。
// trait TCP(var ip : String){}
// ip 仅被视作一个普通参数。
trait TCP(ip : String){
def ipconfig = s"ip : ${ip}"
}
// 可以通过类的主构造器传递参数。
class Device(ip : String) extends TCP(ip)
// 可以直接赋值。
class VM(ip : String) extends TCP("192.168.3.140")
千位分隔符
外国习惯以 "千" 为单位,如 1,000
( thousand,用户通常称 1k ) ,1,000,000
( million,通常简称 1 mio ) 等。对于一些长值字面量,为了增强其可读性,Scala 3 引入了 _
来作为千位分隔符的 ,
。
// 1,000 => 可以表示成 1_000
print (1_000 == 1000)
字面量类型
Scala 3 支持将数值,字符串直接声明为一个类型。比如:
// 声明字面量类型
val NOT_FOUND : "404" = "404"
val OK : "200" = "200"
// "404" | "200" 表示 code 接收 "404" 或者 "200" 字面量。
// 见下文的交类型 intersection type。
def state(code : "404" | "200") : Unit = ()
state(NOT_FOUND) // ok
state(OK) // ok
state("200") // ok
state("500") // error
类型
交类型 Intersection Type
这里的交类型指多个特质之间的交 ( ),因为 Scala 同 Java 一样都是单继承的。
class Person
trait Jumper {def jump() : Unit = ()}
trait Runner {def run() : Unit = ()}
val person1 = new Person with Jumper with Runner
val person2 = new Person with Jumper
val person3 = new Person with Runner
// p 是两个特质的交类型。
def check(p : Jumper & Runner) : Unit = ()
capabilityCheck(person1) // ok
capabilityCheck(person2) // error
capabilityCheck(person3) // error
可以交任意多个特质类型。交类型可以看作是保存了多个特质类型的无序集合,因此它和特质的混入顺序无关,比如:
val person1 = new Person with Jumper with Runner
val person2 = new Person with Runner with Jumper
def check(p : Jumper & Runner) : Unit = ()
check(person1) // ok
check(person2) // ok
并类型 Union Type
这里的 Union 在 Scala 表达为并 ,而非组合。表达 "A 或者 B" 两种类型。
// 该变量接受 Account 或者是 Email 类型进行身份验证。
var idntfy : Account | Email = Account("ljh2077")
// 该函数的 inf 表明允许接受 Account 或者是 Email 类型。
def verify(inf : Account | Email): Unit ={
inf match {
case Account(username) => println(username)
case Email(address) => println(address)
}
}
可以并任意多个类型。并类型和组成类型的排列顺序无关,比如 A | B
等价于 B | A
。
依赖函数类型 Dependent Function Types
Scala 2 中允许通过 def
定义依赖方法 Dependent Methods。下面是官方示例中,对依赖方法 Dependent method 和依赖函数 Dependent function 的描述。
trait Entry { type Key; val key: Key }
def extractKey(e: Entry): e.Key = e.key // a dependent method
val extractor: (e: Entry) => e.Key = extractKey // a dependent function value
// ^^^^^^^^^^^^^^^^^^^
// a dependent function type
称依赖方法的原因是上述的 e.Key
是和 e
相关的路径依赖类型 。但直到 Scala 3 之前,其依赖方法还不能转换为依赖函数,因为 e.Key
没有具体的类型可以描述 ( 注意,路径类型和投影类型 Entry#Key
在语义上是存在差别的 )。Scala 3 则相当于对此做了一个语法糖,它等效于:
Function1[Entry, Entry#Key]:
def apply(e: Entry): e.Key
匹配类型 Match Type
这里直接使用官网的例子说明:通过 Match Type 将 ConstituentPartOf[T]
翻译成不同的类型,具体取决于类型参数 T
。比如:将 BigConstituentPartOf[BigInt]
转换为 Int
,将 BigConstituentPartOf[String]
转换为 Char
。
type ConstituentPartOf[T] = T match
case String => Char // ConstituentPartOf[String] =:= Char
case BigInt => Int // ConstituentPartOf[String] =:= Int
case List[t] => t // ConstituentPartOf[t] =:= t
val v1 : ConstituentPartOf[List[Int]] = 1 // ok
val v2 : ConstituentPartOf[String] = 'a' // ok
val v3 : ConstituentPartOf[BigInt] = 20 // ok
Match Types in Scala 3_哔哩哔哩_bilibili
类型 λ Type Lambda *
类型 Lambda 和 Scala 的高阶类型有关,因此首先需要回顾 Scala 2 中的一些相关概念,下面用一个例子来说明:
class Functor01[M]
class Functor02[M[T]]
其中,Functor01
接收一个类型参数 M
,即 Java 编程中遇到的 "泛型"。在这里,M
本身可以表示为任何类 ( Class ),即不携带类型参数的 Apple
,Fruit
等,或者表示类型 ( Type ),即携带类型参数的 List[X]
,Map[K,V]
,Option[T]
等。
type f1 = Functor01[Apple] // ok, M : Apple
type f2 = Functor01[List[_]] // ok, M : List[_]
type f3 = Functor01[Map[_,_]] // ok, M : Map[_,_]
type f4[T] = Functor01[List[T]] // OK, M : List[T]
而 Functor02
接收一个高阶类型参数 ( Higher-Kind Type ) M[T]
,这相当于约束 M
自身还需要绑定一个类型参数 T
。比如用 M
接收 List
类,那么此时 M[T]
表示 List[T]
类型。
class Apple
class Functor02[M[T]]
type g1 = Functor02[Apple] // error, Apple 没有类型参数
type g2 = Functor02[List] // ok, M[T] => List[T]
type g3 = Functor02[Map] // error, Map 有两个类型参数
一方面,高阶类型使得 Scala 具有抽象复杂类型的能力,比如 T[U[V],M[O,I]]
。另一方面,高阶类型还推迟了类型推断的时机,下面的例子演示了 M
和 T
是如何被先后确定的:
class Functor02[M[T]] :
def fff[T]() : Unit = ()
// 先确定 M 为 List
type g2 = Functor02[List]
// 后确定 T 为 Int
new g2().fff[Int]()
这其中,如果用户不关心 T
的实际类型,那么也可以选择表示为 M[_]
。下面的例子仅确定了 M
的类型为 List
,但是忽略 List
本身携带的类型参数。
class Functor02[M[_]] :
def fff() : Unit = ()
type g0 = Functor02[List]
Functor[M[_]]
不能接收 Map
作为类型参数。原因是: Map
自身携带了两个类型参数,不满足 M[T]
形式。
// Type argument Map[Int, X] does not have the same kind as its bound [K]
// error
type g0[X] = Functor02[Map[Int,X]]
对此需要做一步投影:将携带两个类型参数的 Map
映射为仅携带一个参数的中间类型 IntMap[X]
。
class Functor02[M[T]] :
def fff[T] : Unit = ()
type IntMap[X] = Map[Int,X]
type g1 = Functor02[IntMap]
new g1().fff[Int]
这种类型投影称类型 Lambda。进一步,为了仅使用一行代码完成投影 ,Scala 2 中需要这样使用非常晦涩的表达:
type g1 = Functor02[({type IntMap[X] = Map[Int, X]})#IntMap]
Scala 3 则引入了 =>>
对类型 lambda 进行了一个优化。下面是其简化表述:
type g1 = Functor02[[X]=>>Map[Int,X]]
// 延迟确定了 X 类型为 String
new g1().fff[String]
在这个例子当中,X
类型的上下边界取决于 T
。总结:X
的上界不能比 T
更低,下界不能比 T
更高。即 X
的边界 "包含" 了 T
的边界。
// A >: B >: C >: D >: E
class A
class B extends A
class C extends B
class D extends C
class E extends D
// A >: T >: B
class Functor02[M[T >: D <: B]]
// A >: X >: B
type g2 = Functor02[[X >: E <: A] =>> List[X]]
多态函数类型 Polymorphic Function Types
Scala 3 支持这样定义函数表达式类型,这里举个例子:[K,V] => (K,V) => Map[K,V]
。和普通的 (K,V)=> Map[K,V]
函数类型比,它新增了 [K,V]=>
部分,这表示 "首先确定类型参数 K
,V
;随后再确定 (K,V) => Map[K,V]
类型"。
该特性使得在 Scala 3 中可以直接定义具备类型参数的函数表达式。
// Scala 2
// 只能通过 def 定义泛型方法。
def toMapS2[K,V](k: K,v: V) : Map[K,V] = Map[K,V](k -> v)
// Scala 3
// 柯里化版本
val toMapF2g_0: [K, V] => (K,V) => Map[K, V] =
[K, V] => (key: K,value: V) => Map(key -> value) // good
// 非柯里化版本
val toMapF2g_1: [K, V] => K => V => Map[K, V] =
[K, V] => (key: K) => (value: V) => Map(key -> value) // good
枚举与代数数据类型
Scala 2 中的枚举声明很繁琐,见笔者旧版本的笔记:Scala之:其它类 - 掘金 (juejin.cn)。Scala 3 引入了 enum
关键字来表示枚举类:
// 枚举类可以携带构造参数。
enum Gender(genderID : Int) {
// 这里的所有 case 都是一个枚举,它们默认就是继承 Gender 的。
// 因此没有枚举类不需要参数时,extends Gender 可以省略。
case Male extends Gender(0)
case Female extends Gender(1)
}
基于这种简洁的声明,枚举类可用于构造代数数据类型 Algebraic Data Type ( 缩写为 ADT,它也是抽象数据类型 Abstract data type 的缩写,不要弄混 ),它是函数式编程中的一个重要概念:
enum Tree[T] {
case Leaf(v : T)
case Node(l : Tree[T], r : Tree[T])
}
有关于 ADT 的简要概述,建议参考: 函数范式与领域建模 | 张逸说 (zhangyi.xyz) | 代数数据类型是什么? - 知乎
代数数据类型,故名思意,就是指可以进行代数 Algebra 运算的数据类型,这里的代数运算特指和 ( Sum ) 运算和积运算 ( Product ),用户通过这两种方式组合出可穷尽的,新的数据类型。这里举个例子进行说明:用枚举类表示方位 Direction
和速度 Velocity
:
// 加 var 表示将 tag 视作 Direction 的属性。
enum Direction(var tag : String) :
// 默认情况下 内部的每一个 case 都继承自 Direction.
case East extends Direction("east")
case West extends Direction("west")
case North extends Direction("north")
case South extends Direction("south")
enum Velocity(var kmh : Int) :
case Fast extends Velocity(100)
case Slow extends Velocity(30)
East.tag // "east"
Direction
是 East
,West
,North
,South
的和类型 ( sum type ),因为基本的方位只有四种:东,西,南,北。这有点像前文提到的并类型:Direction
相当于是 East | West | North | South
。这里的 "和" 表示 "或"。同理,Velocity
也是 Fast
和 Slow
的和类型 ( 本例中只对速度进行了 "快" 与 "慢" 的划分 )。
如果对 Direction
和 Velocity
做乘积,我们可以得到两者的积类型 Movement
( 这里的积可以联想笛卡尔积 )。Moving
一共包含了 2 × 4 = 8 个状态,它描述了方位移动的所有可能性:"向东快速移动","向西缓慢移动",......。考虑到不移动的状态 Stay
,通过组合,Movement
一共描述了 8 + 1 = 9 个状态。
enum Movement:
// Moving 是 Direction 和 Velocity 的积类型 product.
case Moving(direction: Direction,velocity: Velocity) extends Movement
case Stay extends Movement
利用 Scala 的模式匹配机制,编译器可以检查出 ADT 的各种潜在分支,并由用户自行决定如何处理它们。
val maybeMoving: Movement = Moving(East,Fast) // Stay, Moving(East,Fast)
maybeMoving match {
case Moving(East,Fast) => println("=>>> fast.")
case Moving(East,Slow) => println("=> slowly.")
case Moving(West,Fast) => println("<<<= fast.")
case Moving(West,Slow) => println("<= slowly.")
case Moving(_,_) => println("moving...")
case Stay => println("staying...")
}
对 implicit 的改进
Scala 3 重点解决了在 Scala 2 中存在的一大痛点问题:随处存在并透明的 implicit
极大地降低了代码的可读性,同时增加了潜在的冲突风险。在 Scala 2 中,implicit
有三个用途:
- 定义隐式值,如
implicit val a = 100
。 - 方法接收隐式参数,如
def f(x : Int)(implicit y : Int) = ...
。 - 定义隐式类实现方法拓展,如
implicit class A(x : B) ...
。
Scala 3 有针对性地将其拆解成三个关键字:given
,using
,以及 extension
。不过,implicit
关键字仍然可以在 Scala 3 中使用。隐式值在 Scala 3 的官方文档中被称之为上下文 ( Context Variable )。
given 关键字
given
关键字用于定义隐式值,其定义方式遵循统一访问原则,因此等式右侧可以是简单的字面量值,也可以是返回值的函数调用。如下:
// 定义了一个隐式值。
// 在一个上下文环境中,隐式值只会被初始化一次。
given Int = Random.nextInt()
// 等价于:
implicit val aInt : Int = Random.nextInt()
given
定义的隐式值 ( 或上下文变量,官方手册中干脆称之为一个 'given' ) 可以不定义变量名,此时由编译器根据其类型来赋予,命名格式上遵循 given_XXX
。为了避免潜在的命名冲突,官方建议主动为 given 赋予名称:
given aRamdomInt : Int = Random.nextInt()
注意,given 的类型是必须声明的。另外需要注意的是,在一个上下文环境中不能出现两个相同类型的 given。
using 闭包 clause
using 则是与 given 相对的概念:如果说 given 是 "定义" 上下文,那么 using 就是 "寻找" 上下文。
def calculateByContext(using ram :Int) : Int = ram % 2
// ^^^^^^^^^^^^^^^^^
// using clause
// 和 Scala 2 一样,在调用函数时,using clause 一般不需要给出。
println(calculateByContext)
函数定义的 (using x1:T1,...)
部分被称之 using 闭包。编译器会自行在上下文中寻找一个 Int
类型的 given,然后赋值给形参 ram
。using clause 可以被定义任意多个。
given Int = 3
def f(using a : Int)(using b : Int)(using c : Int) : Int = a + b + c
println(f) // 9
当用户需要覆盖上下文环境时,也可以在调用函数时主动提供 using 闭包,如下:
println(f(using 1)(using 2)(using 3)) // 6
注意,上述的 using
关键字不可省略。
导入 givens
为了避免潜在的上下文冲突,Scala 3 对上下文的导入操作变得更加谨慎了。现在通过 import
关键字导入某个包或类时,其上下文环境不会一起被加载。
object B :
given Int = 300
given Double = 2.99
def b_f() : Unit = println("")
object A :
// B.* 不包含 300 和 2.99。
import B.*
// f 需要一个 Int given.
def f(using i : Int) : Unit= println(s"imported $i")
@main
def testThis() : Unit =
// 编译不通过,因为缺失 given Int。
f
如果要导入 B 的 givens ,则必须声明为如下格式:
// 只写 given 表示导入 B 的所有上下文
import B.given
// 表示只导入 300
import B.given Int
// 表示导入 300 和 2.99
import B.{given Int,given Double}
// 表示导入 B 的所有内容,包括上下文
import B.{given,*}
上下文函数与建造者模式 *
参考资料见:Context Functions | Scala 3 Language Reference | Scala Documentation (scala-lang.org)
通俗地说,上下文函数指代那些只使用上下文变量的函数。Scala 3 为此引入了新的符号表示 ?=>
,写法如下:
given Int = 3000
val g: Int ?=> String =
($int : Int) ?=> s"Got: ${$int}"
上文的 Int ?=> String
符号表示函数 g
返回 String
类型,而左侧的 ($int : Int)
是一个 using 闭包,编译器将优先从上下文环境中选择一个合适的 Int
值赋给变量 $Int
。
以下三种调用方法都是合法的。其中写法一可以用更简化的写法二代替:
// ?=> 左侧相当于 using clause, 因此需要带关键字 using.
println(g(using given_Int))
// 由于编译器自动从上下文中寻找 given_Int,因此该写法和上面等价。
println(g)
// 主动覆盖上下文环境,打印: Got: 1000.
println(g(using 1000))
g
有更简化的声明方式,那就是省去 ?=>
左侧 "using clause" 部分,直接通过 summon[Int]
提取上下文环境中的 Int
值 ( 该方法类似于 Scala 2 的 implicitly[Int]
方法 )。同理,若调用 g
函数时主动传入 using clause ,summon[Int]
将优先选择传入的值。
val g: Int ?=> String = s"Got: ${summon[Int]}"
// summon[Int] = 20
// Got: 20
println(g(using 20))
上下文函数同普通函数一样可以进行柯里化,携带类型参数等。但是需要注意,上下文函数是 ContextFunctionX
类型。
val gg: ContextFunction1[Int, String] = g
利用上下文函数,控制抽象可以实现精简的建造者模式。下面援引官方的例子:
class Table:
val rows = new ArrayBuffer[Row]
def add(r: Row): Unit = rows += r
override def toString = rows.mkString("Table(", ", ", ")")
class Row:
val cells = new ArrayBuffer[Cell]
def add(c: Cell): Unit = cells += c
override def toString = cells.mkString("Row(", ", ", ")")
case class Cell(elem: String)
构造三个同名的构造器函数,接收用于初始化工作的控制抽象 ( 上下文函数 )。
def table(init: Table ?=> Unit) =
given t: Table = Table()
init
t
def row(init: Row ?=> Unit)(using t: Table) =
given r: Row = Row()
init
t.add(r)
def cell(str: String)(using r: Row) =
r.add(new Cell(str))
现构造 Table
的写法如下:
table(
// 此上下文函数的 $t 由 table 函数内的 given t 提供。
($t : Table) ?=> {
// =>Unit 控制抽象部分,其 row 方法默认使用外部的 $t
row(
// =>Unit 控制抽象部分,其 cell 方法默认使用外部的 $r
($r : Row) ?=> {
cell("r:1 c:1")(using $r)
}
)(using $t)
row(
($r : Row) ?=> {
cell("r:2 c:1")(using $r)
}
)(using $t)
}
)
由于 $t
,$r
均默认由上下文环境提供,因此可以不主动赋 using 闭包。其简写形式为:
val t1: Table = table {
row {cell("r:1 c:1")}
row {cell("r:2 c:1")}
}
这种设计思路类似于 Groovy 中的委托闭包。在这个例子中,其 Table
,row
,cell
的的状态保存与传递就是通过 givens 来实现的。
传名的上下文参数
using clause 允许接收传名调用,如下方代码块的 cxInt
。此时若传入的 x
为 null
,那么 complexInt
实际上就不会被计算。
given complexInt : Int = {
println("init..")
1000
}
// using clause 接收传名调用
def CodeC(x : Int | Null)(using cxInt : =>Int): Int ={
x match {
case xx : Int => xx * cxInt
case _ => 0
}
}
// 打印 init..., complexInt 会被初始化。
CodeC(300)
// 不打印 init..., complexInt 不会被初始化。
CodeC(null)
传名调用和传值调用是一个相对的概念。该部分见:Scala 之:函数式编程之始 - 掘金 (juejin.cn)
extension
Scala 2 中通过 implicit class
以实现在不违背 OCP 原则的前提下对类进行拓展,而 Scala 3 使用 extension
替代之,这使得类拓展语义变得更加明确了。同隐式类一样,extension
关键字后必须要指出拓展的类型。另一点需要注意:extension
不能定义在方法内部。
@main
def test(): Unit =
// '<>' 在 SQL 语句中和 `!=` 等价。
println(1 <> 4)
end test
extension (x : Int)
infix def <>(that : Int) : Boolean = x != that
extension
拓展本身可以携带 using 闭包和泛型。比如:
extension [T](x: T)(using n: Numeric[T])
def + (y: T): T = n.plus(x, y)
参考资料
Scala3出来了,这个语言的前途怎么样? - 知乎 (zhihu.com)
真的学不动了:Scala3 与 Type classes - 知乎 (zhihu.com)
Algebraic Data Types | Scala 3 Language Reference | Scala Documentation (scala-lang.org)
Overview | Scala 3 Language Reference | Scala Documentation (scala-lang.org)
透明 Trait | Scala 3 中文站 (dotty-china.com)
Introducing Transparent Traits in Scala 3 - Knoldus Blogs