模式匹配里的魔法
鲜为人知编译器的工作细节,大多会被我们叫做“魔法”。
接上回,我声明了 type Mutation = (URL, Seq[Element])
,即将特定 URL
页面内容中 新增的若干
Seq[Element]
称之为 Mutation
(变化)。可想而知,因 变化 产生的后续代码逻辑,都得从判断
变化 满足什么条件开始。为了让代码看上去简洁且富表达力,我给
模式匹配 施加一些魔法🪄。
type Callback = Mutation => Unit
def callback: Callback =
case (url, added) => ???
在 URL
中,我最关心是 pathname
1和 search
2两部分。以所有跑步活动的页面链接为例,
其 pathname
为
/modern/activities
,search
是
activityType=running
。然后,我施加第一道,最入门也最核心的魔法
3。
消失的 unapply
object URL:
def unapply(
: URL
u): 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那味了,但远不止如此。接下来,就是要分别判断
pathname
和 search
了,但依旧不用
if
,
def callback: Callback =
case (URL("/modern/activities", "activityType=running"), _) => ???
可能有 Scala 的朋友看到这里,觉得太小儿科吧。别走,下面我要开始装逼了!
注意,以上 search
是最单纯的情况。实际上,它可能是:
activityType=cycling
activityType=running&search=MAF
<empty>
为了简洁又有(表达)力,我写成这样,
def callback: Callback =
case (URL(_, Param["activityType"]("running")), _) => ???
如何,有没有一点小小的震撼?这就是在基础的魔法上,叠加亿点点🤏高阶魔法了。
object Param:
def unapply[S <: String: ValueOf](
: URLSearchParams
sp): 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 (类型约束)7 和
Context 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]
[A, B]: Conversion[
given [A, B],
ExtractPartialFunction[A, B]
] =
.unlift
_
val Attr: LiteralExtract[Element, String] =
=> e =>
k Option.when
(e.hasAttribute(k))
(e.getAttribute(k))
val `href` = Attr(
[Name].value
implicitly)
def callback: Callback =
case (_, added) =>
for case `href`(url) <- added do
???
两点提示:
待续回见。
https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname↩︎
https://developer.mozilla.org/en-US/docs/Web/API/URL/search↩︎
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment↩︎
https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams↩︎
https://docs.scala-lang.org/scala3/book/ca-context-bounds.html#inner-main↩︎
https://contributors.scala-lang.org/t/more-useful-pattern-matching/6751↩︎
https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns#parameterized-active-patterns↩︎
https://docs.scala-lang.org/scala3/book/string-interpolation.html↩︎
https://docs.scala-lang.org/tour/regular-expression-patterns.html#inner-main↩︎
https://hacking-scala.tumblr.com/post/50360896036/regular-expressions-interpolation-in-pattern↩︎
https://docs.scala-lang.org/sips/converters-among-optional-functions-partialfunctions-and-extractor-objects.html#inner-main↩︎