关于泛型我们可能一无所知
类型声明是给编译器的提示词。
接上回,我在模式匹配的雕虫小技,为了是方便不同页面上逻辑处理。这意味着存在多个页面的逻辑,需要我来进行合理的划分和组织。
type Mutation = (URL, Seq[Element])
type Callback = Mutation => Unit
Callback =
given 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
Callback = pageA
given .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
[PageA] =
given Proceedcase (URL("path/a", _), _) =>
???
以上,定义的 PageA
是一个
标记(Marker)类型 ,相当于
java.io.Serializable
这类标记接口(Marker Interface) 2。如法炮制 PageB
,便可
given
两种不同处理类型:Proceed[PageA]
和
Proceed[PageA]
。然后,就有了
// Main.scala
Callback =
given [PageA]
Proceed.orElse(Proceed[PageB])
.orElse(ignore)
咋看上去,还不如上一个版本简洁。别急,如此写法已经有了本质上的跃迁,即从 硬编码 方式,优化成了 依赖注入 的方式。换言之,实现了依赖倒置。当然,以上还不是最终版本,而是为了方便理解最终版的过渡版本。最终,是这样的:
// Main.scala
Callback =
given [(PageA, PageB)] Proceed
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
[EmptyTuple] =
given Proceedcase _ => // ignore
[H, T <: Tuple](using
given [H], Proceed[T]
Proceed): Proceed[H *: T] =
[H] orElse Proceed[T] Proceed
看懂以上组合实现可能不容易,其中存在一个元组叠加的 递归 ,耐心地给自己多一点时间细品,你会和我一样豁然开朗,柳暗花明的。
[(PageA, PageB)]
Proceed=:= Proceed[PageA *: PageB *: EmptyTuple]
=:= Proceed[PageA]
[PageB]
orElse Proceed[EmptyTuple] orElse Proceed
值得注意地是,以上代码出于排版和解读需要,部分做了精简,存在编译错误是情理之中的。有把玩代码需求的,可查看
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/↩︎