1

我正在编写一个 Scala 程序,我希望它可以与两个版本的大型库一起使用。

这个大库的第 2 版对 API 进行了非常小的改动(只有一个类构造函数签名有一个额外的参数)。

// Lib v1
class APIClass(a: String, b:Integer){
...
}

// Lib v2
class APIClass(a: String, b: Integer, c: String){
...
}


// And my code extends APIClass.. And I have no #IFDEF

class MyClass() extends APIClass("x", 1){ //  <--  would be APIClass("x", 1, "y") in library v2
  ...
}

我真的不想分支我的代码。因为那时我需要维护两个分支,明天需要维护 3,4,.. 分支以进行微小的 API 更改:(

理想情况下,我们应该在 Scala 中有一个简单的预处理器,但这个想法很久以前就被 Scala 社区拒绝了。

我真的无法理解的一件事是:Scalameta在这种情况下可以帮助模拟预处理器吗?即有条件地解析两个源文件 - 比如说 - 在编译时已知的环境变量?

如果不是,您将如何解决这个现实生活中的问题?

4

2 回答 2

4

1. C++ 预处理器可以与 Java/Scala 一起使用,如果你之前运行cppjavacscalac(也有Manifold)。


2.如果你真的想在 Scala 中进行条件编译,你可以使用宏注解(在编译时扩展)

宏/src/main/scala/extendsAPIClass.scala

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro paradise")
class extendsAPIClass extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro ExtendsAPIClassMacro.impl
}

object ExtendsAPIClassMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._
    annottees match {
      case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: tail => 
        def updateParents(parents: Seq[Tree], args: Seq[Tree]) = 
          q"""${tq"APIClass"}(..$args)""" +: parents.filter { case tq"scala.AnyRef" => false; case _ => true }

        val parents1 = sys.env.get("LIB_VERSION") match {
          case Some("1") => updateParents(parents, Seq(q""" "x" """, q"1"))
          case Some("2") => updateParents(parents, Seq(q""" "x" """, q"1", q""" "y" """))
          case None      => parents
        }

        q"""
          $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents1 { $self => ..$stats }
          ..$tail
        """
    }
  }
}

核心/src/main/scala/MyClass.scala(如果LIB_VERSION=2

@extendsAPIClass
class MyClass

//Warning:scalac: {
//  class MyClass extends APIClass("x", 1, "y") {
//    def <init>() = {
//      super.<init>();
//      ()
//    }
//  };
//  ()
//}

构建.sbt

ThisBuild / name := "macrosdemo"

lazy val commonSettings = Seq(
  scalaVersion := "2.13.2",
  organization := "com.example",
  version := "1.0.0",
  scalacOptions ++= Seq(
    "-Ymacro-debug-lite",
    "-Ymacro-annotations",
  ),
)

lazy val macros: Project = (project in file("macros")).settings(
  commonSettings,
  libraryDependencies ++= Seq(
    scalaOrganization.value % "scala-reflect" % scalaVersion.value,
  )
)

lazy val core: Project = (project in file("core")).aggregate(macros).dependsOn(macros).settings(
  commonSettings,
  )
)

3.或者,您可以使用Scalameta进行代码生成(在编译时间之前)

构建.sbt

ThisBuild / name := "scalametacodegendemo"

lazy val commonSettings = Seq(
  scalaVersion := "2.13.2",
  organization := "com.example",
  version := "1.0.0",
)

lazy val common = project
  .settings(
    commonSettings,
  )

lazy val in = project
  .dependsOn(common)
  .settings(
    commonSettings,
  )

lazy val out = project
  .dependsOn(common)
  .settings(
    sourceGenerators in Compile += Def.task {
      Generator.gen(
        inputDir = sourceDirectory.in(in, Compile).value,
        outputDir = sourceManaged.in(Compile).value
      )
    }.taskValue,
    commonSettings,
  )

项目/build.sbt

libraryDependencies += "org.scalameta" %% "scalameta" % "4.3.10"

项目/Generator.scala

import sbt._

object Generator {
  def gen(inputDir: File, outputDir: File): Seq[File] = {
    val finder: PathFinder = inputDir ** "*.scala"

    for(inputFile <- finder.get) yield {
      val inputStr = IO.read(inputFile)
      val outputFile = outputDir / inputFile.toURI.toString.stripPrefix(inputDir.toURI.toString)
      val outputStr = Transformer.transform(inputStr)
      IO.write(outputFile, outputStr)
      outputFile
    }
  }
}

项目/Transformer.scala

import scala.meta._

object Transformer {
  def transform(input: String): String = {
    val (v1on, v2on) = sys.env.get("LIB_VERSION") match {
      case Some("1") => (true, false)
      case Some("2") => (false, true)
      case None      => (false, false)
    }
    var v1 = false
    var v2 = false
    input.tokenize.get.filter(_.text match {
      case "// Lib v1" =>
        v1 = true
        false
      case "// End Lib v1" =>
        v1 = false
        false
      case "// Lib v2" =>
        v2 = true
        false
      case "// End Lib v2" =>
        v2 = false
        false
      case _ => (v1on && v1) || (v2on && v2) || (!v1 && !v2)
    }).mkString("")
  }
}

common/src/main/scala/com/api/APIClass.scala

package com.api

class APIClass(a: String, b: Integer, c: String)

在/src/main/scala/com/example/MyClass.scala

package com.example

import com.api.APIClass

// Lib v1
class MyClass extends APIClass("x", 1)
// End Lib v1

// Lib v2
class MyClass extends APIClass("x", 1, "y")
// End Lib v2

out/target/scala-2.13/src_managed/main/scala/com/example/MyClass.scala

sbt out/compile如果之后LIB_VERSION=2

package com.example

import com.api.APIClass

class MyClass extends APIClass("x", 1, "y")

用于覆盖 Scala 函数的 toString 的宏注释

如何在scala中合并多个导入?

于 2020-05-14T20:14:59.657 回答
3

I see some options but none if them is "conditional compilation"

  • you can create 2 modules in your build - they would have a shared source directory and each of them you have a source directory for code specific to it. Then you would publish 2 versions of your whole library
  • create 3 modules - one with your library and an abstract class/trait that it would talk to/through and 2 other with version-specific implementation of the trait

The problem is - what if you build the code against v1 and user provided v2? Or the opposite? You emitted the bytecode but JVM expects something else and it all crashes.

Virtually every time you have such compatibility breaking changes, library either refuses to update or fork. Not because you wouldn't be able to generate 2 versions - you would. Problem is in the downstream - how would your users deal with this situation. If you are writing an application you can commit to one of these. If you are writing library and you don't want to lock users to your choices... you have to publish separate version for each choice.

Theoretically you could create one project, with 2 modules, which share the same code and use different branches like #ifdef macros in C++ using Scala macros or Scalameta - but that is a disaster if you want to use IDE or publish sourcecode that your users can use in IDE. No source to look at. No way to jump to the definition's source. Disassembled byte code at best.

So the solution that you simply have separate source directories for mismatching versions is much easier to read, write and maintain in a long run.

于 2020-05-14T12:35:27.757 回答