Scala 条件编译 [英] Scala conditional compilation

查看:72
本文介绍了Scala 条件编译的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写一个 Scala 程序,我希望它能够与一个大库的两个版本一起使用.

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

//Lib v1class APIClass(a: String, b:Integer){...}//库 v2class APIClass(a: String, b: Integer, c: String){...}//我的代码扩展了 APIClass .. 而且我没有 #IFDEFclass MyClass() extends APIClass("x", 1){//<-- 将是库 v2 中的 APIClass("x", 1, "y")...}

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

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

我无法真正理解的一件事是:Scalameta 可以帮助模拟预处理器吗?这个案例?IE.有条件地将两个源文件解析为 - 比如说 - 在编译时已知的环境变量?

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

解决方案

1. 如果在 之前运行 cpp,C++ 预处理器可以与 Java/Scala 一起使用javacscalac(还有 Manifold).

<小时>

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

宏/src/main/scala/extendsAPIClass.scala

import scala.annotation.{StaticAnnotation, compileTimeOnly}导入 scala.language.experimental.macros导入 scala.reflect.macros.blackbox@compileTimeOnly("启用宏天堂")class extendsAPIClass extends StaticAnnotation {def macroTransform(annottees: Any*): Any = macro ExtendsAPIClassMacro.impl}对象扩展APIClassMacro {def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {导入 c.universe._注释匹配 {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" =>错误的;案例_ =>真的 }val parents1 = sys.env.get("LIB_VERSION") 匹配 {case Some("1") =>updateParents(parents, Seq(q""" "x" """, q"1"))case Some("2") =>updateParents(parents, Seq(q""" "x" """, q"1", q""" "y" """))情况无=>父母}q"""$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents1 { $self =>..$stats }..$尾"""}}}

core/src/main/scala/MyClass.scala(如果 LIB_VERSION=2)

@extendsAPIClass我的课堂//警告:scalac:{//class MyClass extends APIClass("x", 1, "y") {//def () = {//super.();//()//}//};//()//}

build.sbt

ThisBuild/name := "macrosdemo"惰性 val commonSettings = Seq(ScalaVersion := "2.13.2",组织:=com.example",版本:=1.0.0",scalacOptions ++= Seq("-Ymacro-debug-lite","-Ymacro-注释",),)惰性 val 宏: Project = (project in file("macros")).settings(通用设置,libraryDependencies ++= Seq(scalaOrganization.value % "scala-reflect" % scalaVersion.value,))懒惰的核心核心:项目=(文件中的项目(核心")).聚合(宏).dependsOn(宏).设置(通用设置,))

<小时>

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

build.sbt

ThisBuild/name := "scalametacodegendemo"惰性 val commonSettings = Seq(ScalaVersion := "2.13.2",组织:=com.example",版本:=1.0.0",)懒惰的 val 共同 = 项目.设置(通用设置,)懒惰的 val = 项目.dependsOn(常见).设置(通用设置,)懒惰的 val out = 项目.dependsOn(常见).设置(编译中的 sourceGenerators += Def.task {发电机.gen(inputDir = sourceDirectory.in(in, Compile).value,outputDir = sourceManaged.in(Compile).value)}.taskValue,通用设置,)

project/build.sbt

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

project/Generator.scala

import sbt._对象生成器{def gen(inputDir: File, outputDir: File): Seq[File] = {val finder: PathFinder = inputDir ** "*.scala"for(inputFile <- finder.get) 产量 {val inputStr = IO.read(inputFile)val outputFile = outputDir/inputFile.toURI.toString.stripPrefix(inputDir.toURI.toString)val outputStr = Transformer.transform(inputStr)IO.write(outputFile, outputStr)输出文件}}}

project/Transformer.scala

import scala.meta._对象转换器{def 变换(输入:字符串):字符串 = {val (v1on, v2on) = sys.env.get("LIB_VERSION") 匹配 {case Some("1") =>(真假)case Some("2") =>(假,真)情况无=>(假的,假的)}无功 v1 = 假var v2 = 假input.tokenize.get.filter(_.text 匹配{case "//Lib v1" =>v1 = 真错误的case "//End Lib v1" =>v1 = 假错误的case "//Lib v2" =>v2 = 真错误的case "//End Lib v2" =>v2 = 假错误的案例_ =>(v1on && v1) ||(v2on && v2) ||(!v1 && !v2)}).mkString("")}}

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

包com.apiclass APIClass(a: String, b: Integer, c: String)

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

package com.example导入 com.api.APIClass//库 v1class MyClass 扩展 APIClass("x", 1)//结束库 v1//库 v2class MyClass 扩展 APIClass("x", 1, "y")//结束 Lib v2

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

(在sbt out/compile之后,如果LIB_VERSION=2)

package com.example导入 com.api.APIClassclass MyClass 扩展 APIClass("x", 1, "y")

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

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

I'm writing a Scala program and I want it to work with two version of a big library.

This big library's version 2 changes the API very slightly (only one class constructor signature has an extra parameter).

// 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
  ...
}

I really don't want to branch my code. Because then I'd need to maintain two branches, and tomorrow 3,4,..branches for tiny API changes :(

Ideally we'd have a simple preprocessor in Scala, but the idea was rejected long ago by Scala community.

A thing I don't really couldn't grasp is: can Scalameta help simulating a preprocessor in this case? I.e. parsing two source files conditionally to - say - an environmental variable known at compile time?

If not, how would you approach this real life problem?

解决方案

1. C++ preprocessors can be used with Java/Scala if you run cpp before javac or scalac (also there is Manifold).


2. If you really want to have conditional compilation in Scala you can use macro annotation (expanding at compile time)

macros/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
        """
    }
  }
}

core/src/main/scala/MyClass.scala (if LIB_VERSION=2)

@extendsAPIClass
class MyClass

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

build.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. Alternatively you can use Scalameta for code generation (at the time before compile time)

build.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,
  )

project/build.sbt

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

project/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
    }
  }
}

project/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)

in/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

(after sbt out/compile if LIB_VERSION=2)

package com.example

import com.api.APIClass

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

Macro annotation to override toString of Scala function

How to merge multiple imports in scala?

这篇关于Scala 条件编译的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆