似乎既支持完整的 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 同时拥有继承、实现、混入的作用,且 class、trait、enum、object、sealed class/trait 都可以使用 extends、with 关键字.
一般而言,可认为 extends A, B 和 extends 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 {}
given 与 using
本身用于上下文中隐式传递和接收参数,类似于 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 实例)方法,且因为 Show2 有 extension 故可以直接调用类型类实现.
对于 Animal3:
assert(ani3.show == "Method of Animal3 ani3") // OOP Interface
// 使用 Show3 的 given 实例:
assert(summon[Show3].show == "Show3")
}
伴生对象
当 object 无同名 class、case class、enum、trait 作时为单例模式,有则是伴生对象.
- 对于
class而言相当于定义传统 OOP 中的静态方法、属性 - 对于
trait和enum可用于定义默认方法等 - // 可认为伴生对象描述的是类型层与值层的object同名后的现象,实际运作语义没有区别,但给使用者带来了更好的主观体验
object为仅值层(除非obj.type),而trait仅类型层,class、case class、enum为值层与类型层
匿名对象与单例对象
匿名对象:当 {} 前加 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
})()