Header Image

Scala3 观察日志1:类、类型类、对象、隐式实例、函数式

创建时间:2026-01-22T10:45
更新时间:2026-01-22T16:50
4 次阅读 • 1 条评论 • 0 人喜欢

似乎既支持完整的 OOP 与完整的 FP 是件很酷的事,但实际上…会被混乱不堪的写法吓晕

读前须知

本文适合至少对函数式编程或 Haskell 或 Rust 或 Idris2 或 Scala3 或 Lean4 等语言有少许了解的人.

出于文章性质考虑,本文将使用英文句号 . 代替所有中文句号 .

Currying 与不完全调用

Scala 函数支持多参数列表:

def normal_add(a: Int, b: Int): Int = a + b
def curry_add(a: Int) (b: Int): Int = a + b
assert(normal_add(233,233) == curry_add(233)(233))

不完全调用:

val add_one = curry_add(1)
val add_one_ = normal_add(1, _) // 不完全调用
assert(add_one(2) == add_one_(2))

一词多义

历史原因,Scala 的 extends 同时拥有继承、实现、混入的作用,且 classtraitenumobjectsealed class/trait 都可以使用 extendswith 关键字.

一般而言,可认为 extends A, Bextends A with B 等价,且 Scala3 中前者逐渐成为主流:

trait A {}
trait B {}
trait C {}

class Class1 extends A, B {}
class Class2 extends A with B {}

with 可以有多个,但 extends 不行:

class Class3 extends A with B with C {}
// class Class3_ extends A extends B with C {}

givenusing

本身用于上下文中隐式传递和接收参数,类似于 Haskell 的 ask 或传统 OOP 中的依赖注入,不过大部分时候专门针对于类型类.

given Int = 233
def using_int(using x: Int): Int = x
assert(using_int == 233)

可以为 given 实例取名,且对于同一类型可以有多个 given 实例:

def using_string(using x: String): String = x
given String = "aaa"
// given String = "bbb" 但这样不行
given s2: String = "bbb

错误,有多个 given 实例:

println(using_string)

必须手动传递隐式参数(非匿名):

assert(using_string(using s2) == "bbb")

类似于 Idris2:

interface Show' a where
show' : a -> String

Show' String where
show' x = "Str is " ++ x

[impl2] Show' String where
show' x = "My str: " ++ x

putStr' : a -> { auto n: Show' a } -> IO ()

main : IO ()
main = do
putStr' "Hello" -- Str is Hello
putStr' "Hello" {n=impl2} -- My str: Hello

可恶,Shiki 居然不支持 Idris 的高亮,这里只好借用一下 Haskell 的高亮了.

using 是针对当前参数列表下所有参数的:

def using_int_string(using i: Int, s: String): Unit = () // `i` 和 `s` 都是隐式的

且必须写在最前面进行修饰:

// `def using_int_string_and_more(x: Int, using i: Int, s: String): Unit = ()` 语法错误
def using_int_string_and_more(x: Int) (using i: Int, s: String): Unit = ()

trait 实现语法糖:

class Class {}
trait T[A] {
  def f(a: A): String
}
given T[Class] with {
  def f(a: Class) = ""
}

其实是语法糖,等价于:

given impossible: T[Class] = new T[Class] {
  def f(a: Class) = ""
}

还可以简写:

given impossible2: T[Class] = (a: Class) => ""

trait 的不同行为

类型类定义(传统):

trait Show1[A] {
  def show(a: A): String
}

class Animal1(val name: String) extends Show1[Animal1] {
  def show(a: Animal1): String = s"Method of Animal1 $name"
}

given Show1[Animal1] with {
  def show(a: Animal1) = s"Show1: ${a.name}"
}

类型类定义(Scala3 新兴,extension 方便使用):

trait Show2[A] {
extension (a: A) def show: String
}

// 实现 Show2 的 Animal2:
class Animal2(val name: String) extends Show2[Animal2] {
  extension (a: Animal2) def show: String = s"Method of Animal2 $name"
}

given Show2[Animal2] with {
  extension (a: Animal2) def show: String = s"Show2: ${a.name}"
}

// 不实现 Show2 的 Animal2
class Animal2_(val name: String) {}

given Show2[Animal2_] with {
  extension (a: Animal2_) def show: String = s"Show2: ${a.name}"
}

OOP Interface:

trait Show3 {
  def show: String
}

// OOP
class Animal3(val name: String) extends Show3 {
  def show: String = s"Method of Animal3 $name"
}

// 语义合法,但几乎无实际意义:
given Show3 with {
  def show: String = "Show3"
}
def test_trait(): Unit = {
  val ani1 = new Animal1("ani1")
  val ani2 = new Animal2("ani2")
  val ani2_ = new Animal2_("ani2_")
  val ani3 = new Animal3("ani3")

传统无 extension 类型类实现的使用需要通过 summon 函数:

assert(summon[Show1[Animal1]].show(ani1) == "Show1: ani1")
assert(ani1.show(new Animal1("another ani1")) == "Method of Animal1 ani1") // 此处传入的 `Animal1` 不影响

对于 Animal2

assert(summon[Show2[Animal2]].show(ani2) == "Show2: ani2") // `given` 实例结果
assert(ani2.show(new Animal2("another ani2")) == "Method of Animal2 ani2") // 调用实例实现的 `show` 方法,此处传入的 `Animal2` 不影响
assert(ani2_.show == "Show2: ani2_") // `Animal2_` 并没有实现 `Show2` 故此处是 `given` 实例结果

故可知当类实现某方法后会覆盖掉类型类的同名实现(given 实例)方法,且因为 Show2extension 故可以直接调用类型类实现.

对于 Animal3

assert(ani3.show == "Method of Animal3 ani3") // OOP Interface

// 使用 Show3 的 given 实例:
  assert(summon[Show3].show == "Show3")
}

伴生对象

object 无同名 classcase classenumtrait 作时为单例模式,有则是伴生对象.

  • 对于 class 而言相当于定义传统 OOP 中的静态方法、属性
  • 对于 traitenum 可用于定义默认方法等 - // 可认为伴生对象描述的是类型层与值层的 object 同名后的现象,实际运作语义没有区别,但给使用者带来了更好的主观体验

object 为仅值层(除非 obj.type),而 trait 仅类型层,classcase classenum 为值层与类型层

匿名对象与单例对象

匿名对象:当 {} 前加 new 为创建匿名对象,否则作为块表达式.

val Obj1: { def pi: Int; val pi2: Int; def add(x: Int, y: Int): Int } = new {
  println("Init obj1") // 赋值时立即出发
  val pi2: Int = 222 // 只触发一次
  def pi: Int = 111 // 多次,相当于 TypeScript 的 `getter`
  // `var pi3: Int = 333` 不允许可变
  def add(x: Int, y: Int): Int = x + y
}

关于匿名对象的可变性:匿名对象中 var 不可写入到(Obj1 的)类型注解,不允许可变(即使是 var Obj1,即使是非顶层作用域).

object 单例模式(没有被作为伴生对象):

object Obj2 {
  println("Init obj2") // `object` 懒加载,只有访问时会触发,且只触发(初始化)一次
  val pi2: Int = 222
 def pi: Int = 111
 var pi3: Int = 333 // `object` 允许可变
 def add(x: Int, y: Int): Int = x + y
}

密封类与代数数据类型

传统写法:

sealed abstract class Expr {
  def eval: Int
  val brand = "Expr" // 提供默认实现
}

case class Add(a: Expr, b: Expr) extends Expr {
  def eval: Int = a.eval + b.eval // `case class` 必须实现 `sealed abstract class` 所有方法
  override val brand = "Add Expr" // 也可以覆盖(即 OOP 中的重写)
}

case class Num(num: Int) extends Expr {
  def eval: Int = num
  def isZero: Boolean = num == 0 // `case class` 也可以有自己的方法( `enum` 语法糖做不到)
}

// 无参数的值构造子
case object Pi extends Expr {
  def eval: Int = 3
}

assert(Add(Num(1), Add(Num(2), Num(3))).eval == 6)

case class 为数据类,类似于 kotlin 的 data class 和 Rust 的 struct

Scala3 enum 语法糖:

enum Expr_ {
  case Add(a: Expr, b: Expr)
  case Num(num: Int)
  case Pi

  def eval: Int = this match {
    case Expr_.Num(n) => n
    case Expr_.Add(a, b) => a.eval + b.eval
    case Expr_.Pi => 3
  }
}

enum 语法糖似乎会将值构造子、构造子方法都塞伴生对象里,所以此处要加个前缀 Expr_.

关于类型收窄

val expr: Expr = Num(Pi.eval)
// `expr.isZero` 不可使用,因为还未确定具体类型
val isZero = expr match {
  case n: Add  if n.eval == 0 => true
  case n: Num => n.isZero // 此时可用,此即为 **类型收窄**
  case _ => false
}

但不同于 TypeScript,外层的 expr 类型依旧为 Expr,只是只知道了具体的新声明 n: Num,而 TypeScript 中则会重置原值的类型(大概率是 TypeScript 独家特性了):

type Expr = { _tag: "Num", eval: number, isZero: boolean } | { _tag: "Add", eval: number } | { _tag: "Pi", eval: number }

const Num = (n: number): Expr => ({ _tag: "Num", get eval() { return n }, get isZero() { return n === 0 } })
const Add = (a: number, b: number): Expr => ({ _tag: "Add", get eval() { return a + b } })
const Pi: Expr = { _tag: "Pi", get eval() { return 3 } }

const expr = Num(Pi.eval)

const isZero = (() => {
  // 此时 `expr: Expr`
  if (expr._tag === 'Num') return expr.isZero // 现在 `expr: { _tag: "Num"; eval: number; isZero: boolean; }`
  if (expr._tag === 'Add') return expr.eval === 0
  return false
})()
本文链接:
版权声明:本文由作者原创,禁止任何形式转载。
技术 #Scala #FP #函数式编程 #类型类
点个赞 赏杯咖啡 分享到...

评论区

共 1 条评论

avatar
@romi 2026-01-22 16:51

非常好 使用旋转

回复

Made with ❤️ by Arimura Sena

RSS 订阅网站地图友情链接