Scala 3 新特性一览

Scala 3 的下载方式,见:Scala 3 (epfl.ch)

Scala 3 的编译器代号为 Dotty。总体而言,Scala 3 的改动可概括如下:

  1. 支持无括号风格的代码编写 。
  2. 对类型系统做了极大丰富。
  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

这里的交类型指多个特质之间的交 ( \cap ),因为 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 表达为并 \cup ,而非组合。表达 "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 *

参考:scala中的 Type Lambda - 蒋航 - 博客园 (cnblogs.com)

Type lambdas and kind projector - Underscore

类型 Lambda 和 Scala 的高阶类型有关,因此首先需要回顾 Scala 2 中的一些相关概念,下面用一个例子来说明:

class Functor01[M]
class Functor02[M[T]]
复制代码

其中,Functor01 接收一个类型参数 M,即 Java 编程中遇到的 "泛型"。在这里,M 本身可以表示为任何类 ( Class ),即不携带类型参数的 AppleFruit 等,或者表示类型 ( 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]]。另一方面,高阶类型还推迟了类型推断的时机,下面的例子演示了 MT 是如何被先后确定的:

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]]
复制代码

泛函参数类型

Scala 3: Type Lambdas, Polymorphic Function Types, and Dependent Function Types | by Dean Wampler | Scala 3 | Medium

Scala 3 支持这样定义函数表达式类型,这里举个例子:[K,V] => (K,V) => Map[K,V]。和普通的 (K,V)=> Map[K,V] 函数类型比,它新增了 [K,V]=> 部分,这表示 "首先确定类型参数 KV;随后再确定 (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"
复制代码

DirectionEastWestNorthSouth 的和类型 ( sum type ),因为基本的方位只有四种:东,西,南,北。这有点像前文提到的并类型:Direction 相当于是 East | West | North | South。这里的 "和" 表示 "或"。同理,Velocity 也是 FastSlow 的和类型 ( 本例中只对速度进行了 "快" 与 "慢" 的划分 )。

如果对 DirectionVelocity 做乘积,我们可以得到两者的积类型 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 有三个用途:

  1. 定义隐式值,如 implicit val a = 100
  2. 方法接收隐式参数,如 def f(x : Int)(implicit y : Int) = ...
  3. 定义隐式类实现方法拓展,如 implicit class A(x : B) ...

Scala 3 有针对性地将其拆解成三个关键字:givenusing,以及 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)

Context is King. Context functions in Scala 3 | by Adam Warski | SoftwareMill Tech Blog | SoftwareMill Tech Blog

通俗地说,上下文函数指代那些只使用上下文变量的函数。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 中的委托闭包。在这个例子中,其 Tablerowcell 的的状态保存与传递就是通过 givens 来实现的。

传名的上下文参数

using clause 允许接收传名调用,如下方代码块的 cxInt。此时若传入的 xnull ,那么 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)

Dotty 中的新功能 - 知乎 (zhihu.com)

透明 Trait | Scala 3 中文站 (dotty-china.com)

Introducing Transparent Traits in Scala 3 - Knoldus Blogs

猜你喜欢

转载自juejin.im/post/7061227510884909063