欢迎光临

Scala 3 上下文抽象实战:从 given/using 到类型类派生与编译期依赖注入的完整指南

Scala 3 对 Scala 2 最饱受争议的特性——隐式(implicit)——进行了全面重构,统称为「上下文抽象」(Contextual Abstractions)。这不仅仅是语法糖层面的改进,而是对类型类派生、编译期依赖注入、上下文参数传递等核心编程范式的系统性重新设计。本文将深入 Scala 3 上下文抽象的每个角落,从基础的 given/using 语法到高级的类型类自动派生,配合大量可运行代码,帮助你彻底掌握这一强大的语言特性。

Scala 3 上下文抽象编程

一、从 implicit 到 given/using:范式转移

Scala 2 的 implicit 机制功能强大但语法混乱——同一个

1
implicit

关键字被用于隐式参数、隐式转换、隐式类和隐式值四种完全不同的场景。Scala 3 将这些职责拆分为多个清晰的关键字:

Scala 2 Scala 3 用途
1
implicit val
1
given
定义上下文实例
1
implicit val
1
using
声明上下文参数
1
implicit def
1
given ... with
定义上下文实例(带实现)
1
implicit class
1
extension
扩展方法
1
implicit def

(转换)

1
Conversion
隐式转换(需显式)
1
implicitly[T]
1
summon[T]
材料化上下文实例

下面是一个最简单的对比示例。Scala 2 写法:


1
2
3
4
5
6
7
8
9
10
11
12
13
// Scala 2
trait Show[A] {
  def show(a: A): String
}

implicit val showInt: Show[Int] = (a: Int) => a.toString
implicit val showStr: Show[String] = (a: String) => s'"' + a + '"'

def printIt[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))

printIt(42)       // 输出: 42
printIt("hello")  // 输出: "hello"

Scala 3 等价写法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Scala 3
trait Show[A]:
  def show(a: A): String

given Show[Int] with
  def show(a: Int): String = a.toString

given Show[String] with
  def show(a: String): String = "'" + a + "'"

// 也可以用简洁的单行写法
given Show[Boolean]: (a: Boolean) => a.toString

def printIt[A](a: A)(using s: Show[A]): Unit =
  println(s.show(a))

printIt(42)       // 输出: 42
printIt("hello")  // 输出: 'hello'
printIt(true)     // 输出: true

关键变化:参数列表用

1
using

标记而非

1
implicit

;实例定义用

1
given

引导,无需

1
val

/

1
def

关键字。语义一致但可读性大幅提升。

二、given 实例的多种定义形式

Scala 3 的

1
given

支持多种定义形式,适用于不同场景。理解它们的区别对于编写整洁的 Scala 3 代码至关重要。

2.1 匿名 given

当不需要在代码中按名称引用实例时,可以使用匿名 given:


1
2
3
4
5
6
7
// 匿名 given — 编译器自动生成名称
given Show[Int] = (a: Int) => a.toString
given Show[String] = (a: String) => "'" + a + "'"

// 匿名 given with 实现块
given Show[List[String]] with
  def show(a: List[String]): String = a.mkString("[", ", ", "]")

匿名 given 的名称由编译器自动生成(通常是

1
given_Show_Int

这样的形式)。如果只需要通过类型查找,匿名 given 足够。但当你需要在某些场景显式传递它时,就需要命名。

2.2 命名 given


1
2
3
4
5
6
7
// 命名 given — 可以显式引用
given showInt: Show[Int] = (a: Int) => a.toString
given showStr: Show[String] with
  def show(a: String): String = "'" + a + "'"

// 显式传递实例
printIt(42)(using showInt)

2.3 条件 given(Conditional Givens)

这是 Scala 3 上下文抽象中最强大的特性之一——可以定义「在满足某些条件时才可用」的 given 实例,类似于 Scala 2 的隐式参数推导链:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当存在 Show[A] 时,自动派生 Show[List[A]]
given showList[A](using ev: Show[A]): Show[List[A]] with
  def show(a: List[A]): String =
    a.map(ev.show).mkString("[", ", ", "]")

// 当存在 Show[A] 和 Show[B] 时,派生 Show[(A, B)]
given showTuple[A, B](using sa: Show[A], sb: Show[B]): Show[(A, B)] with
  def show(t: (A, B)): String = "(" + sa.show(t._1) + ", " + sb.show(t._2) + ")"

// 现在,只要有基础实例,复合类型的 Show 就自动可用
printIt(List(1, 2, 3))         // 输出: [1, 2, 3]
printIt(List("a", "b"))        // 输出: ['a', 'b']
printIt((42, "hello"))         // 输出: (42, 'hello')
printIt(List(List(1), List(2))) // 输出: [[1], [2]] — 递归派生!

条件 given 的推导是递归的——编译器会自动构建推导链。这在实际工程中极大减少了样板代码。

三、using 参数与上下文传递

3.1 using 子句的基本用法

1
using

标记的参数列表在调用时可以省略,编译器会自动查找匹配的 given 实例:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
trait Logger:
  def log(msg: String): Unit

trait Repository[A]:
  def findById(id: Long): Option[A]
  def save(a: A): Unit

case class User(id: Long, name: String, email: String)

class UserService(using logger: Logger, userRepo: Repository[User]):
  def getUser(id: Long): Option[User] =
    logger.log("Fetching user " + id)
    userRepo.findById(id)

  def updateUser(user: User): Unit =
    logger.log("Updating user " + user.id)
    userRepo.save(user)

// 定义 given 实例
given Logger with
  def log(msg: String): Unit = println("[LOG] " + msg)

given Repository[User] with
  private val store = scala.collection.mutable.Map.empty[Long, User]
  def findById(id: Long): Option[User] = store.get(id)
  def save(a: User): Unit = store(a.id) = a

// 创建服务时,Logger 和 Repository 自动注入
val service = UserService()
service.updateUser(User(1, "Alice", "alice@example.com"))
service.getUser(1) // 输出: [LOG] Fetching user 1

3.2 using 参数的显式传递

虽然编译器会自动查找 given 实例,但你始终可以显式传递

1
using

参数:


1
2
3
// 显式传递 using 参数
val customLogger: Logger = (msg: String) => println("[CUSTOM] " + msg)
val service2 = UserService(using customLogger, summon[Repository[User]])

3.3 using 参数的省略与按类型传递


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 多个相同类型的 using 参数 — 必须显式或通过名称区分
trait Cache:
  def get(key: String): Option[String]
  def set(key: String, value: String): Unit

class DualCacheService(using primary: Cache, fallback: Cache):
  def get(key: String): String =
    primary.get(key).orElse(fallback.get(key)).getOrElse("not found")

// 编译器可以通过参数名区分相同类型的实例
given primaryCache: Cache with
  private val store = scala.collection.mutable.Map.empty[String, String]
  def get(key: String): Option[String] = store.get(key)
  def set(key: String, value: String): Unit = store(key) = value

given fallbackCache: Cache with
  def get(key: String): Option[String] = None
  def set(key: String, value: String): Unit = ()

四、extension 方法:替代隐式类

Scala 3 用

1
extension

关键字替代了 Scala 2 的隐式类,语法更加明确,且支持泛型约束:


1
2
3
4
5
6
7
8
9
10
11
// 为 String 添加扩展方法
extension (s: String)
  def slugify: String =
    s.toLowerCase.trim.replaceAll("\\s+", "-")

  def truncate(maxLen: Int): String =
    if s.length <= maxLen then s else s.substring(0, maxLen - 3) + "..."

// 使用
println("Hello World!".slugify)           // 输出: hello-world
println("A very long string here".truncate(10))  // 输出: A very...

4.1 泛型扩展方法


1
2
3
4
5
6
7
// 泛型扩展,带上下文约束
extension [A](list: List[A])(using show: Show[A])
  def prettyPrint: String =
    list.map(show.show).mkString("List(", ", ", ")")

// 只有当 Show[Int] 在作用域内时才能调用
println(List(1, 2, 3).prettyPrint)  // 输出: List(1, 2, 3)

4.2 运算符扩展


1
2
3
4
5
6
7
8
9
// 自定义运算符
extension (x: Int)
  def %% (y: Int): Boolean = x % y == 0

extension (str: String)
  def &lt;-&gt; (other: String): String = str + " <-> " + other

println(12 %% 3)             // 输出: true
println("left" &lt;-&gt; "right")  // 输出: left <-> right

五、类型类模式:从定义到派生

类型类(Type Class)是 Scala 函数式编程的核心模式。Scala 3 的上下文抽象让类型类的定义和派生变得异常简洁。

5.1 定义完整的类型类


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 定义类型类 trait
trait JsonWriter[A]:
  def write(a: A): String

// 2. 定义伴生对象,包含 given 实例和辅助方法
object JsonWriter:
  // 基础类型实例
  given JsonWriter[Int] = (a: Int) => a.toString
  given JsonWriter[String] = (a: String) => "'" + a + "'"
  given JsonWriter[Boolean] = (a: Boolean) => a.toString
  given JsonWriter[Double] = (a: Double) => a.toString

  // 容器类型实例(条件 given)
  given optWriter[A](using w: JsonWriter[A]): JsonWriter[Option[A]] with
    def write(a: Option[A]): String = a match
      case None    => "null"
      case Some(v) => w.write(v)

  given listWriter[A](using w: JsonWriter[A]): JsonWriter[List[A]] with
    def write(a: List[A]): String =
      a.map(w.write).mkString("[", ",", "]")

  // 辅助方法
  def toJson[A](a: A)(using w: JsonWriter[A]): String = w.write(a)

// 3. 使用
import JsonWriter.*
println(toJson(42))                // 输出: 42
println(toJson(List(1, 2, 3)))     // 输出: [1,2,3]
println(toJson(Option("hello")))   // 输出: 'hello'

5.2 为自定义类型实现类型类


1
2
3
4
5
6
7
8
9
10
11
case class Product(id: Long, name: String, price: Double, inStock: Boolean)

object Product:
  given JsonWriter[Product] = (p: Product) =>
    "{"id":" + p.id + ","name":"" + p.name +
    "","price":" + p.price + ","inStock":" + p.inStock + "}"

import JsonWriter.*
val p = Product(1, "Scala Book", 39.99, true)
println(toJson(p))
// 输出: {"id":1,"name":"Scala Book","price":39.99,"inStock":true}

5.3 类型类语法扩展


1
2
3
4
5
6
7
8
9
10
// 通过 extension 让类型类方法看起来像原生方法
extension [A](a: A)(using w: JsonWriter[A])
  def toJson: String = w.write(a)

// 现在 toJson 可以像方法调用一样使用
println(42.toJson)                   // 输出: 42
println("hello".toJson)              // 输出: 'hello'
println(List(1, 2, 3).toJson)        // 输出: [1,2,3]
println(Product(2, "Monitor", 299.0, false).toJson)
// 输出: {"id":2,"name":"Monitor","price":299.0,"inStock":false}

六、编译期依赖注入实战

Scala 3 的上下文抽象天然适合编译期依赖注入(Compile-time DI),无需任何框架即可实现类型安全的依赖管理。下面是一个完整的实战示例:

编译期依赖注入架构


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// === 领域模型 ===
case class Account(id: String, balance: BigDecimal, currency: String)
case class Transfer(from: String, to: String, amount: BigDecimal)

// === 服务接口 ===
trait AccountRepository:
  def find(id: String): Option[Account]
  def update(account: Account): Unit

trait ExchangeRateService:
  def rate(from: String, to: String): BigDecimal

trait AuditLogger:
  def log(event: String): Unit

trait NotificationService:
  def notify(accountId: String, message: String): Unit

// === 业务服务 ===
class TransferService(
  using repo: AccountRepository,
        rates: ExchangeRateService,
        audit: AuditLogger,
        notifier: NotificationService
):
  def transfer(t: Transfer): Either[String, Unit] =
    for
      from <- repo.find(t.from).toRight("Source account not found")
      to   <- repo.find(t.to).toRight("Target account not found")
      _    <- if from.balance >= t.amount then Right(())
              else Left("Insufficient funds")
    yield
      val convertedAmount =
        if from.currency != to.currency then
          t.amount * rates.rate(from.currency, to.currency)
        else t.amount

      repo.update(from.copy(balance = from.balance - t.amount))
      repo.update(to.copy(balance = to.balance + convertedAmount))
      audit.log("Transfer " + t.amount + " from " + t.from + " to " + t.to)
      notifier.notify(t.from, "Sent " + t.amount + " " + from.currency)
      notifier.notify(t.to, "Received " + convertedAmount + " " + to.currency)

// === 生产环境 given 实例 ===
object Production:
  given AccountRepository with
    private val store = scala.collection.mutable.Map(
      "acc1" -> Account("acc1", BigDecimal("10000.00"), "USD"),
      "acc2" -> Account("acc2", BigDecimal("5000.00"), "EUR")
    )
    def find(id: String): Option[Account] = store.get(id)
    def update(account: Account): Unit = store(account.id) = account

  given ExchangeRateService with
    def rate(from: String, to: String): BigDecimal = (from, to) match
      case ("USD", "EUR") => BigDecimal("0.92")
      case ("EUR", "USD") => BigDecimal("1.09")
      case _              => BigDecimal("1.0")

  given AuditLogger with
    def log(event: String): Unit = println("[AUDIT] " + event)

  given NotificationService with
    def notify(accountId: String, message: String): Unit =
      println("[NOTIFY] " + accountId + ": " + message)

// === 使用 ===
import Production.{*, given}
val service = TransferService()
val result = service.transfer(Transfer("acc1", "acc2", BigDecimal("1000")))
println(result)
// 输出:
// [AUDIT] Transfer 1000 from acc1 to acc2
// [NOTIFY] acc1: Sent 1000 USD
// [NOTIFY] acc2: Received 920.00 EUR

这种方式的依赖注入有以下优势:

  • 编译期安全:缺少任何依赖都会在编译时报错,而非运行时
  • 零反射:所有依赖在编译期解析,无运行时开销
  • 环境隔离:通过 import 不同对象来切换生产/测试环境
  • 无需框架:纯语言特性,不依赖 Guice、Spring 等外部库
  • IDE 友好:类型签名即文档,IDE 可以精确追踪依赖来源

七、given 实例的作用域与导入

Scala 3 对 given 的导入机制进行了精细化控制,解决了 Scala 2 中隐式导入不可控的问题。

7.1 选择性导入


1
2
3
4
5
6
7
// 只导入 given 实例(不导入其他成员)
import Production.{*, given}
// 或者显式列出类型
import Production.{given AccountRepository, given ExchangeRateService}

// 只导入非 given 成员
import Production.{*, given as _}

7.2 按类型导入


1
2
3
4
5
// 按类型导入 given — 只导入特定类型的实例
import Production.{given AccountRepository}

// 导入多个类型的 given
import Production.{given AccountRepository, given AuditLogger}

7.3 包级 given 与组织策略


1
2
3
4
5
6
7
8
9
10
// 在包对象中定义全局默认 given
package object myapp:
  given defaultLogger: Logger with
    def log(msg: String): Unit = println("[APP] " + msg)

  given defaultTimeProvider: TimeProvider with
    def now: java.time.Instant = java.time.Instant.now()

// 在任何地方导入即可使用
import myapp.{given Logger, given TimeProvider}

最佳实践是将 given 实例按层次组织:基础设施层的 given 定义在包对象中(默认日志、时间等),领域层的 given 定义在伴生对象中,应用层通过 import 组合所需实例。

八、summon 与上下文材料化

Scala 3 用

1
summon

替代了 Scala 2 的

1
implicitly

,语义更清晰,且支持更复杂的材料化场景:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基本 summon — 按类型获取 given 实例
val intShow: Show[Int] = summon[Show[Int]]

// 在 given 定义中使用 summon 引用其他实例
given Show[Map[String, Int]]: (m: Map[String, Int]) =>
  m.map { case (k, v) =>
    summon[Show[String]].show(k) + ": " + summon[Show[Int]].show(v)
  }.mkString("{", ", ", "}")

// 条件 summon — 利用类型约束
def requireShow[A](using ev: Show[A]): Show[A] = ev

// 使用 inline 和 summon 实现编译期验证
inline def requireNonEmpty[T](inline value: T): T =
  inline value match
    case s: String =>
      require(s.nonEmpty, "String must not be empty")
      s
    case _ => value

九、opaque 类型与上下文抽象的配合

Scala 3 的 opaque 类型是对 Scala 2 AnyVal 新类型模式的替代,配合上下文抽象可以实现零开销的领域类型:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
object DomainTypes:
  // 定义 opaque 类型 — 编译期为不同类型,运行时零开销
  opaque type UserId = Long
  opaque type Email = String

  // 在伴生作用域内提供转换和验证
  object UserId:
    def apply(id: Long): UserId =
      require(id > 0, "UserId must be positive")
      id
    extension (uid: UserId) def value: Long = uid

  object Email:
    def apply(raw: String): Email =
      require(raw.matches("^[^@]+@[^@]+$"), "Invalid email format")
      raw
    extension (e: Email) def value: String = e

// 使用
import DomainTypes.*
val uid = UserId(123)
val email = Email("alice@example.com")

// 编译期类型安全 — 不会混淆 UserId 和 Long
def sendNotification(userId: UserId, addr: Email): Unit =
  println("Sending to user " + userId.value + " at " + addr.value)

sendNotification(uid, email)  // 编译通过
// sendNotification(email, uid)  // 编译错误:类型不匹配

十、实战:构建类型安全的配置系统

结合上下文抽象、opaque 类型和类型类,构建一个完整的类型安全配置读取系统:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// === 配置类型类 ===
trait ConfigReader[A]:
  def read(config: Map[String, String], key: String): Either[String, A]

object ConfigReader:
  given ConfigReader[String] = (config, key) =>
    config.get(key).toRight("Missing key: " + key)

  given ConfigReader[Int] = (config, key) =>
    config.get(key)
      .toRight("Missing key: " + key)
      .flatMap(v => v.toIntOption.toRight("Invalid Int: " + v))

  given ConfigReader[Boolean] = (config, key) =>
    config.get(key)
      .toRight("Missing key: " + key)
      .flatMap {
        case "true" | "yes" | "1" => Right(true)
        case "false" | "no" | "0" => Right(false)
        case v => Left("Invalid Boolean: " + v)
      }

  given ConfigReader[List[String]] = (config, key) =>
    config.get(key)
      .map(_.split(",").map(_.trim).toList)
      .toRight("Missing key: " + key)

  // 条件派生 — Option 包装
  given optReader[A](using r: ConfigReader[A]): ConfigReader[Option[A]] =
    (config, key) => config.get(key) match
      case None => Right(None)
      case Some(_) => r.read(config, key).map(Some(_))

// === 配置 DSL ===
class ConfigParser(config: Map[String, String]):
  def get[A](key: String)(using reader: ConfigReader[A]): Either[String, A] =
    reader.read(config, key)

// === 使用 ===
@main def configDemo(): Unit =
  val rawConfig = Map(
    "app.name" -> "MyApp",
    "app.port" -> "8080",
    "app.debug" -> "true",
    "app.hosts" -> "host1,host2,host3",
    "app.timeout" -> "30"
  )

  val parser = ConfigParser(rawConfig)
  import ConfigReader.{*, given}

  val name: Either[String, String]     = parser.get[String]("app.name")
  val port: Either[String, Int]        = parser.get[Int]("app.port")
  val debug: Either[String, Boolean]   = parser.get[Boolean]("app.debug")
  val hosts: Either[String, List[String]] = parser.get[List[String]]("app.hosts")
  val timeout: Either[String, Option[Int]] = parser.get[Option[Int]]("app.missing")

  println("Name: " + name)     // Right(MyApp)
  println("Port: " + port)     // Right(8080)
  println("Debug: " + debug)   // Right(true)
  println("Hosts: " + hosts)   // Right(List(host1, host2, host3))
  println("Timeout: " + timeout) // Right(None)

十一、性能考量与最佳实践

11.1 given 实例的性能特征

定义方式 运行时开销 适用场景
匿名 given(简单值) 零(内联) 基础类型实例
given … with(带方法) 对象分配 复杂类型类实例
条件 given(带 using 参数) 对象分配 + 查找 递归派生
inline given 零(编译期展开) 关键路径性能优化

11.2 最佳实践清单

  • 优先使用命名 given:当实例可能在测试中被覆盖时,命名 given 更易于显式替换
  • 将 given 定义在伴生对象中:这样无需显式 import 就在作用域内
  • 避免隐式转换:Scala 3 的
    1
    Conversion

    需要显式导入,默认不启用——保持这个习惯

  • 使用 extension 而非 given Conversion:扩展方法语义更清晰
  • 限制条件 given 的递归深度:过深的推导链会影响编译速度
  • 用 opaque 类型替代 AnyVal 新类型:零开销且语义更精确
  • 为 given 实例编写文档:它们是隐式契约,文档化尤为重要

总结

Scala 3 的上下文抽象是对语言设计的重大改进。通过将 Scala 2 笼统的

1
implicit

拆分为

1
given

1
using

1
extension

等语义明确的关键字,不仅提升了代码可读性,更减少了隐式解析中的歧义。条件 given 让类型类派生变得自然而强大,编译期依赖注入无需任何框架即可实现生产级的架构设计。

掌握这些特性的关键在于实践——尝试用 Scala 3 重写你现有的类型类、依赖注入和配置管理代码,你会真切感受到样板代码的减少和类型安全的提升。Scala 3 上下文抽象不是简单的语法糖,而是一种让你以更少的代码表达更精确意图的编程范式。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Scala 3 上下文抽象实战:从 given/using 到类型类派生与编译期依赖注入的完整指南
分享到: 更多 (0)