V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sadhen
V2EX  ›  程序员

Scala 元编程:伊甸园初窥

  •  
  •   sadhen ·
    darcy-shen · 2019-01-01 17:12:51 +08:00 · 1884 次点击
    这是一个创建于 2157 天前的主题,其中的信息可能已经有所发展或是发生改变。

    阅读建议

    本文的行文风格不求阅读意义上的可读性,而是期望读者能够跟着本文的一些探索,自己做一些尝试,即 git clone 本文涉及的代码阅读并实践。

    至于 Scala 元编程的一些介绍,请阅读 @王在祥 的《神奇的 Scala Macro 之旅系列》: , , ,

    绕不开的 Sbt

    我们从Macro Paradise 的例子开始。有点遗憾的是,这个例子仍然在使用旧的 Sbt。所以,我们的第一步是把构建的定义升级到当前 Sbt 的最新版。完整的项目见我 fork 的sbt-example-paradise

    首先,在 project/build.properties 中指定:

    sbt.version=1.2.7
    

    然后,再修改 build.sbt 为:

    val paradiseVersion = "2.1.0"
    
    lazy val commonSettings = Seq(
      scalaVersion := "2.12.8",
      addCompilerPlugin("org.scalamacros" % "paradise" % paradiseVersion cross CrossVersion.full)
    )
    
    lazy val root = (project in file("."))
      .aggregate(core, macros)
    
    lazy val macros = (project in file("macros"))
      .settings(commonSettings)
      .settings(
        libraryDependencies ++= Seq(
          "org.scala-lang" % "scala-reflect" % scalaVersion.value
        )
      )
    
    lazy val core = (project in file("core"))
      .settings(commonSettings)
      .dependsOn(macros)
    

    我们可以对比一下这一段 SBT 项目构建的定义和 Maven 的构建定义:

    1. commonSettings 相当于 Maven 里面最顶层的 pom.xml
    2. root 相当于 Maven 中指定子模块的那几行
    3. macros 和 core 分别对应两个子目录,其中,core 依赖 macros。

    最后,运行一下这个例子:

    $ sbt
    > project core # 切换到 core 子项目
    > compile
    > run
    

    输出结果如下:

    hello
    

    你好

    @hello
    object Test extends App {
      println(this.hello)
    }
    

    我们实际上运行的这段代码异常简洁。this 指代 Test 这个独立对象(stand-alone object)本身,调用了一个不存在的方法 hello。我们的问题是,@hello 施放了什么样的魔法,生成了这样一个不存在的 hello 方法。

    忽略别的语法细节,我们只看下面的这段代码:

    annottees.map(_.tree).toList match {
            case q"object $name extends ..$parents { ..$body }" :: Nil =>
              q"""
                object $name extends ..$parents {
                  def hello: ${typeOf[String]} = "hello"
                  ..$body
                }
              """
    }
    

    从直观的感受,我们能猜想到,$name即 Test,$parents即 App,$body就是代码的主体。parents 和 body 前面有两个点,区别于 name。

    通过$name$parents$body这种特殊的语法形式,我们实际上把:

    object Test extends App {
      println(this.hello)
    }
    

    变换成了:

    object Test extends App {
      println(this.hello)
    
      def hello: String = "hello"
    }
    

    尽管我们或许不知道其中的语法所对应的语义,更不清楚具体的实现机制,但这部分代码的可读性是非常棒(intuitive)的。

    下一步?

    现在大概知道了@hello所施放的黑魔法。下一步,我们就得弄明白这个简单的例子中,每一行代码的含义。

    否则,任何拙劣的模仿和尝试,都是在浪费时间。

    那我们应该如何学习这些黑魔法呢?官网的文档可读性并不好,而且不少是过时的。网络上也没有特别友好的面向新人的教程。

    追本溯源,前面的项目实际上涉及到两个子项目,scala-reflect 和 paradise。在 scala 的源代码中,scala-reflect 相关的代码单元测试并不多,所以我们从 paradise 的单元测试开始阅读。

    git clone [email protected]:scalamacros/paradise.git
    

    可以将 sbt 的版本统一到 1.2.7。这样做,主要为了防止去下载另外一个 Sbt 的版本,浪费大量时间。

    很幸运,更改版本之后,项目可以正常编译,测试。

    $ sbt
    > compile
    > project tests
    > test
    

    这个 sbt 的终端保持开启,然后用 Intelli Idea 打开整个项目,这样,应该能够更快地打开整个项目,我们在 Sbt 的会话中可以看到无端跳出来的日志:

    [info] new client connected: network-1
    

    大致浏览一下这些单元测试的代码,可以获得一些初步的印象。

    Macro Paradise 将在 Scala 2.13.x 中内置

    另外,这个paradise 插件将在 Scala 2.13.x 中内置,所以我们还需要看一下 Scala 2.13.x 分支的代码。通过git grep paradise,可以看到一些蛛丝马迹。paradise 的源代码主要被引入到了 compiler 和 reflect 下面,而单元测试则是在 tests/macro-annot 下面。

    此时,我们可以将前面的 sbt-example-paradise 升级到 Scala 2.13.x:

    val paradiseVersion = "2.1.0"
    
    lazy val commonSettings = Seq(
      scalaVersion := "2.13.0-M5",
      scalacOptions ++= Seq("-Ymacro-annotations")
    )
    
    lazy val root = (project in file("."))
      .aggregate(core, macros)
    
    lazy val macros = (project in file("macros"))
      .settings(commonSettings)
      .settings(
        libraryDependencies ++= Seq(
          "org.scala-lang" % "scala-reflect" % scalaVersion.value
        )
      )
    
    lazy val core = (project in file("core"))
      .settings(commonSettings)
      .dependsOn(macros)
    

    为了避免构建定义太复杂,我们直接新开一个分支

    这里请注意一下编译选项scalacOptions ++= Seq("-Ymacro-annotations")。我是在 Scala 源代码中通过git grep paradise瞥见的这个编译选项,然后简单看了一下相关代码,了解到了其中的作用。这边第二次提及git grep,是因为在日常工作中,发现一些小伙伴不知道有git grep这么好用的工具,觉得十分诧异。

    不过细想也很正常,很多时候,我们自己所认为的 Common Sense,别人极有可能根本不了解。

    所以,我们直接研究最新的 2.13.x,不需要任何依赖,就可以探索 Scala 的元编程。

    小结

    本文从一个 Macro Paradise 项目的示例项目,从构建和代码阅读的细节入手,从大体上去感知 Macro Paradise 的某个具体的应用场景。

    4 条回复    2019-04-01 19:53:24 +08:00
    hepin1989
        1
    hepin1989  
       2019-01-02 00:22:54 +08:00
    写的非常用心,赞一个。
    Subfire
        2
    Subfire  
       2019-04-01 14:33:46 +08:00
    sadhen
        3
    sadhen  
    OP
       2019-04-01 19:52:07 +08:00
    @Subfire 这个是我本人在 juejin 发的,哈哈
    sadhen
        4
    sadhen  
    OP
       2019-04-01 19:53:24 +08:00
    其实最先是在知乎专栏上写的,我应该在这里贴一下目录:

    https://zhuanlan.zhihu.com/p/50189343
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1032 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 21:01 · PVG 05:01 · LAX 13:01 · JFK 16:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.