𝚲

模式匹配里的魔法

鲜为人知编译器的工作细节,大多会被我们叫做“魔法”。

接上回,我声明了 type Mutation = (URL, Seq[Element]) ,即将特定 URL 页面内容中 新增的若干 Seq[Element] 称之为 Mutation (变化)。可想而知,因 变化 产生的后续代码逻辑,都得从判断 变化 满足什么条件开始。为了让代码看上去简洁且富表达力,我给 模式匹配 施加一些魔法🪄。

type Callback = Mutation => Unit

def callback: Callback = 
  case (url, added) => ???

URL 中,我最关心是 pathname 1search 2两部分。以所有跑步活动的页面链接为例,

插入速心比走势的活动页

pathname/modern/activitiessearchactivityType=running。然后,我施加第一道,最入门也最核心的魔法 3

消失的 unapply

object URL:
  def unapply(
    u: URL
  ): Option[(String, String)] = 
    Some(u.pathname, u.search)

unapply 是一个特殊命名的方法,如同魔法的咒语,施法效果为

def callback: Callback = 
  case (URL(pathname, search), _) => ???

给不明觉厉的朋友翻译翻译,就是:

def callback: Callback = (url, _) =>
  val opt = URL.unapply(url) 
  if opt.isDefined then
    val (pathname, search) = opt.get
    ???

简言之,调用 unapply ,判断返回值,若存在则赋值并继续,这三个步骤精炼为了一行代码。有点 解构赋值 4那味了,但远不止如此。接下来,就是要分别判断 pathnamesearch 了,但依旧不用 if

def callback: Callback = 
  case (URL("/modern/activities", "activityType=running"), _) => ???

可能有 Scala 的朋友看到这里,觉得太小儿科吧。别走,下面我要开始装逼了!

注意,以上 search 是最单纯的情况。实际上,它可能是:

为了简洁又有(表达)力,我写成这样,

def callback: Callback = 
  case (URL(_, Param["activityType"]("running")), _) => ???

如何,有没有一点小小的震撼?这就是在基础的魔法上,叠加亿点点🤏高阶魔法了。

object Param:
  def unapply[S <: String: ValueOf](
    sp: URLSearchParams 
  ): Option[String] = 
    val key = valueOf[S]
    Option.when
      (sp.has(key))
      (sp.get(key))

为了配合 Param ,解构 URL 时需要用 searchParams 5,比 search 实现更简单。

其中涉及一个冷门知识点: Literal-based singleton types (字面单例类型)6。简言之,使用 valueOf 可以获取字面单例类型 "activityType" 的字符串值(也就是它自身)。当然,还需要辅以 [S <: String: ValueOf] 的声明,这又涉及到知识点 Type Bounds (类型约束)7Context Bounds (上下文绑定) 8

以上写法源自 Scala Contributors 里「More Useful Pattern Matching」的帖子 9。贴主是想讨论 Scala 要不要支持 Parameterized Active Patterns (参数化激活模式)10。可惜楼歪了,引发了一场有趣的代码竞赛,即便是有 Lihaoyi 回帖正楼也无事于补,感兴趣的朋友参见脚注链接。

String Interpolate 的意外之用

作为第一个回帖的人,我可能有一点点歪楼的责任😂。但 String Interpolate (字符串插值)11也可用于模式匹配,这绝对是 Scala Doc 都没提到的“冰系”魔法。

还是拿 pathname 举例说明。假设,我要匹配个人资料页,

/modern/profile/{id}

代码可以这么写,

def callback: Callback = 
  case (URL(s"/modern/profile/$id", _), _) => ???

熟悉 Scala 的朋友,咋看可能会有点「地铁老人看手机」,其实细想想还挺符合直觉的用法。既然都说到这了,不妨再容我炫个十多年前的雕虫小技。

def callback: Callback = 
  case (URL(r"/modern/profile/(\\d+)$id", _), _) => ???

r 不是 StringContext 内置的,是通过 Regex 来扩展实现的 12

import scala.util.matching.Regex

extension (sc: StringContext)
  def r = Regex(sc.parts.mkString)

这个技巧是从 Hacking Scala 上学来的13,在大量字符数据处理场景中相当好用,省去很多无聊的声明定义语句,这也是「More Useful Pattern Matching」帖子背后的痛点。

魔法不为炫技

当年在厂内使用这些魔法,没少被同学背地里吐槽😂,真是苦了他们了。我使用的初衷,当然不是为了炫技,而是让代码简洁有力。只是用上头了,难免把握不好度,在团队协同效率上造成额外隐形成本。

这不是一个用或不用的二元选择,而是一种权衡。今天,魔法我依然在用,只不过用的时候考虑的更多些。

文末彩蛋

import scala.languageFeature.implicitConversions
import sourcecode.Name

type Extract[A, B] 
  = A => Option[B]

type LiteralExtract[A, B]
  = String => Extract[A, B]

given [A, B]: Conversion[
  Extract[A, B], 
  PartialFunction[A, B]
] = 
  _.unlift

val Attr: LiteralExtract[Element, String] = 
  k => e =>
    Option.when
      (e.hasAttribute(k))
      (e.getAttribute(k))

val `href` = Attr(
  implicitly[Name].value
)          

def callback: Callback = 
  case (_, added) =>
    for case `href`(url) <- added do
      ???

两点提示:

待续回见。


  1. https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname↩︎

  2. https://developer.mozilla.org/en-US/docs/Web/API/URL/search↩︎

  3. https://docs.scala-lang.org/tour/extractor-objects.html↩︎

  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment↩︎

  5. https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams↩︎

  6. https://docs.scala-lang.org/sips/42.type.html↩︎

  7. https://docs.scala-lang.org/tour/upper-type-bounds.html↩︎

  8. https://docs.scala-lang.org/scala3/book/ca-context-bounds.html#inner-main↩︎

  9. https://contributors.scala-lang.org/t/more-useful-pattern-matching/6751↩︎

  10. https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns#parameterized-active-patterns↩︎

  11. https://docs.scala-lang.org/scala3/book/string-interpolation.html↩︎

  12. https://docs.scala-lang.org/tour/regular-expression-patterns.html#inner-main↩︎

  13. https://hacking-scala.tumblr.com/post/50360896036/regular-expressions-interpolation-in-pattern↩︎

  14. https://github.com/com-lihaoyi/sourcecode/tree/main↩︎

  15. https://docs.scala-lang.org/sips/converters-among-optional-functions-partialfunctions-and-extractor-objects.html#inner-main↩︎