𝚲

关于泛型我们可能一无所知

类型声明是给编译器的提示词。

图片来自网络

接上回,我在模式匹配的雕虫小技,为了是方便不同页面上逻辑处理。这意味着存在多个页面的逻辑,需要我来进行合理的划分和组织。

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 的定义下,pageApageB 是同类不同名的两个实例。有了幻影类型后,则让编译器可以在类型上区分二者。

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 上先后叠加 PageBPageA。有没有和列表 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 中不少重大特性。

待续回见。


  1. https://www.deviantart.com/giadina96/art/GoT-you-know-nothing-Jon-Snow-376890774↩︎

  2. https://en.wikipedia.org/wiki/Marker_interface_pattern↩︎

  3. https://www.scala-lang.org/2021/02/26/tuples-bring-generic-programming-to-scala-3.html↩︎

  4. https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/core/Proceed.scala↩︎

  5. https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/plotly/Axis.scala↩︎

  6. https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/garmin/read/package.scala↩︎

  7. https://github.com/hanabix/mpb/blob/356ad801f9964ceba42f2338f9f9a7f55373a774/src/plotly/Trace.scala↩︎

  8. https://corecursive.com/008-generic-programming-and-shapeless-with-miles-sabin/↩︎

  9. https://github.com/milessabin/shapeless↩︎