关于泛型我们可能一无所知
类型声明是给编译器的提示词。
接上回,我在模式匹配的雕虫小技,为了是方便不同页面上逻辑处理。这意味着存在多个页面的逻辑,需要我来进行合理的划分和组织。
type Mutation = (URL, Seq[Element])
type Callback = Mutation => Unit
given Callback =
case (URL("path/a", _), _) =>
???
case (URL("path/b", _), _) =>
???
case _ => // ignore将它们放置在一个 Callback
里,显然是最朴素直白地。咋看起来,还有种简洁的美感呢。嗯,所有的代码粪池,都从无知乐观开始的。
相信略有见识的同学会和我想到一块去,那就是先将每个页面的逻辑抽离到一个独立的模块(文件)里,再在
Callback 里将其组合起来。在 Scala 里,我们采用
PartialFunction[A, B] 要来做这点还是很容易的。
PartialFunction[A, B] 可以理解能够处理部分
A 的函数 A => B ,即当输入类型为
A 参数时,可能成功返回类型 B 的值
;亦或是,会抛出 scala.MatchError
的异常,表示着当前参数值此函数无法处理。
type Proceed = PartialFunction[Mutation, Unit]
// PageA.scala
val pageA: Proceed =
case (URL("path/a", _), _) =>
???
// PageB.scala
val pageA: Proceed =
case (URL("path/b", _), _) =>
???// Main.scala
given Callback = pageA
.orElse(pageB)
.orElse(ignore)
def ignore: Process =
case _ => // ignore 第一步,在不同的文件模块中实现不同页面的处理逻辑 Proceed
(PartialFunction 的别名)。第二步,用 orElse
方法将多个页面逻辑组合在一起。注意,不要忘了最后那个需要
ignore 的处理。
至此,问题看似被完美的处理了,但本篇的主题还未有触及。显然,这里还有更佳通用的处理方式。
Phantom Type
直译为 幻影类型。很玄乎的样子,还是来看代码吧,
type Proceed[A] =
PartialFunction[Mutation, Unit]
object Proceed:
def apply[A](using pa: Proceed[A]) = pa 泛化的类型别名 Proceed 有个类型参数
A,它(在等式左边)看得见,却(在等式右边)用不着,这就叫
幻影类型。
这能有什么意义呢?嗯,这对编译器来说非常有意义。回看前文的
Proceed 的定义下,pageA 和 pageB
是同类不同名的两个实例。有了幻影类型后,则让编译器可以在类型上区分二者。
Marker Type
// PageA.scala
trait PageA
given Proceed[PageA] =
case (URL("path/a", _), _) =>
??? 以上,定义的 PageA 是一个
标记(Marker)类型 ,相当于
java.io.Serializable 这类标记接口(Marker Interface) 2。如法炮制 PageB,便可
given 两种不同处理类型:Proceed[PageA] 和
Proceed[PageA]。然后,就有了
// Main.scala
given Callback =
Proceed[PageA]
.orElse(Proceed[PageB])
.orElse(ignore)咋看上去,还不如上一个版本简洁。别急,如此写法已经有了本质上的跃迁,即从 硬编码 方式,优化成了 依赖注入 的方式。换言之,实现了依赖倒置。当然,以上还不是最终版本,而是为了方便理解最终版的过渡版本。最终,是这样的:
// Main.scala
given Callback =
Proceed[(PageA, PageB)]Tuples & Generic Programming
在 Scala 3 中,元组(Tuple)类型引入一个非常重大的改变,这使得泛化(或通用)编程(Generic Programming)更为容易 3。这个改变的核心是,有了 空元组(EmptyTuple),它的价值相当于数字 零。
(PageA, PageB) =:= PageA *: PageB *: EmptyTuple也就是说,二元组 (PageA, PageB),等价于在
EmptyTuple 上先后叠加 PageB 和
PageA。有没有和列表 1 :: 2 :: Nil
很像?像,就对了!无生一,一生二,二生无穷。这意味着,我们可以实现基于类型的组合啊。
// EmptyTuple extends Tuple
given Proceed[EmptyTuple] =
case _ => // ignore
given [H, T <: Tuple](using
Proceed[H], Proceed[T]
): Proceed[H *: T] =
Proceed[H] orElse Proceed[T]看懂以上组合实现可能不容易,其中存在一个元组叠加的 递归 ,耐心地给自己多一点时间细品,你会和我一样豁然开朗,柳暗花明的。
Proceed[(PageA, PageB)]
=:= Proceed[PageA *: PageB *: EmptyTuple]
=:= Proceed[PageA]
orElse Proceed[PageB]
orElse Proceed[EmptyTuple]值得注意地是,以上代码出于排版和解读需要,部分做了精简,存在编译错误是情理之中的。有把玩代码需求的,可查看
Proceed 的完整代码 4。此外,代码仓库还有更多泛化编程实际应用案例可供参考
5 6 7。
泛化编程是很值得玩味的,它让我对于 组合 的理解又上升了一个层次,以上内容远不及全部。有兴趣深入的同学,我非常推荐去听听访谈 Miles Sabin 的播客 8,他是 Scala 泛化编程库 shapeless 9 的作者,进而深远地影响了 Scala 3 中不少重大特性。
待续回见。
https://www.deviantart.com/giadina96/art/GoT-you-know-nothing-Jon-Snow-376890774↩︎
https://www.scala-lang.org/2021/02/26/tuples-bring-generic-programming-to-scala-3.html↩︎
https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/core/Proceed.scala↩︎
https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/plotly/Axis.scala↩︎
https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/garmin/read/package.scala↩︎
https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/plotly/Trace.scala↩︎
https://corecursive.com/008-generic-programming-and-shapeless-with-miles-sabin/↩︎
