SBT 项目的跨版本编译

近期的工作中,需要在 Spark 任务中做一些字符串处理。但比较尴尬的是,过去的 Jaskell Core,我一直是以 2.13 为目标开发的,而现在 Spark的各种发布版,普遍使用的是 2.11 和 2.12 。

再专门为 2.11 和 2.12 编写独立的项目,就非常不经济了,这个时候,要把 Jaskell 变成跨版本项目。

SBT 通过 cross version 设置支持跨版本。我们首先设定 supportScalaVersions :

lazy val scala213 = "2.13.6"
lazy val scala212 = "2.12.15"
lazy val supportedScalaVersions = List(scala213, scala212)

version := "0.7.3"
scalaVersion := scala213

通过 supportedScalaVersions 和 scalaVersion 的配置,我们指明:

  • 这个项目支持 Scala 2.12 和 Scala 2.13
  • 默认版本为 scala 2.13
  • 2.12 配置为 2.12.15
  • 2.13 配置为 2.13.6

开发工具,例如 Intellij,会根据 scalaVersion 配置编译器。现在它会按照 scala 2.13 的语法处理项目代码。如果切换编译器,可以修改这个变量,然后刷新项目。

如果我们想要在命令行执行sbt 任务,例如执行单元测试,只需要用 ++ 选项指明编译器版本,例如我们如果想用 Scala 2.12 测试 Jaskell Core,可以执行:

sbt ++2.12.15 test

通过测试,我发现我对 Parsec 的 Monad 实现并不能用于 Scala 2.12:

object Parsec {
  def apply[E, T](parser: State[E] => Try[T]): Parsec[E, T] = parser(_)

  implicit def toFlatMapper[E, T, O](binder: Binder[E, T, O]): (T)=>Parsec[E, O] = binder.apply

  implicit def mkMonad[T]: Monad[({type P[A] = Parsec[T, A]})#P] =
    new Monad[({type P[A] = Parsec[T, A]})#P] {
      override def pure[A](element: A): Parsec[T, A] = Return(element)

      override def fmap[A, B](m: Parsec[T, A], f: A => B): Parsec[T, B] = m.ask(_).map(f)

      override def flatMap[A, B](m: Parsec[T, A], f: A => Parsec[T, B]): Parsec[T, B] = state => for {
        a <- m.ask(state)
        b <- f(a).ask(state)
      } yield b
    }

}

按照 Scala 2的语法设计,typeclass 可以通过 implicit class 实现。但是 implicit class 只能支持将目标class 作为泛型参数。于是我这里采用了一个迂回的方法,先定义了一个基于 implicit 变量的抽象类作为 ops 类型:

  abstract class MonadOps[A, M[_]](implicit I: Monad[M]) {
    def self: M[A]
    
    def map[B](f: A => B): M[B] = I.fmap(self, f)

    def <:>[B](f: A => B): M[B] = I.fmap(self, f)

    def flatMap[B](f: A => M[B]): M[B] = I.flatMap(self, f)

    def liftA2[B, C](f: (A, B) => C): M[B] => M[C] = m => I.liftA2(f)(self, m)

// ...

然后通过一系列隐式转换规则,将不同的类型映射为对应的 Monad 。例如对 List 和 Seq 和 Try 的映射:

  implicit val listMonad: Monad[List] = new Monad[List] {
    override def pure[A](element: A): List[A] = List(element)

    override def fmap[A, B](m: List[A], f: A => B): List[B] = m.map(f)

    override def flatMap[A, B](m: List[A], f: A => List[B]): List[B] = m.flatMap(f)
  }


  implicit val seqMonad: Monad[Seq] = new Monad[Seq] {
    override def pure[A](element: A): Seq[A] = Seq(element)

    override def fmap[A, B](m: Seq[A], f: A => B): Seq[B] = m.map(f)

    override def flatMap[A, B](m: Seq[A], f: A => Seq[B]): Seq[B] = m.flatMap(f)
  }

  implicit val tryMonad: Monad[Try] = new Monad[Try] {
    override def pure[A](element: A): Try[A] = Success(element)

    override def fmap[A, B](m: Try[A], f: A => B): Try[B] = m.map(f)

    override def flatMap[A, B](m: Try[A], f: A => Try[B]): Try[B] = m.flatMap(f)
  }

而 Parsec 的映射比较特殊,严格来说 Parsec[E, T] 并不应该成为 Monad ,它是一个 E=>T 的算子,记录的是计算规则。 Monad 化的应该是它的计算结果,即 Try[T] 。但是在 Haskell 中我们可以借助 curry 语法干净的实现 Parsec 算子的组装。在 Scala 中这就比较困难。

所以我借助 Type Lambda 语法,强行将 Parsec[E, T] 封装成 Monad[T] 类型,模拟了 Haskell 的效果:

object Parsec {
  def apply[E, T](parser: State[E] => Try[T]): Parsec[E, T] = parser(_)

  implicit def toFlatMapper[E, T, O](binder: Binder[E, T, O]): (T)=>Parsec[E, O] = binder.apply

  implicit def mkMonad[T]: Monad[({type P[A] = Parsec[T, A]})#P] =
    new Monad[({type P[A] = Parsec[T, A]})#P] {
      override def pure[A](element: A): Parsec[T, A] = Return(element)

      override def fmap[A, B](m: Parsec[T, A], f: A => B): Parsec[T, B] = m.ask(_).map(f)

      override def flatMap[A, B](m: Parsec[T, A], f: A => Parsec[T, B]): Parsec[T, B] = state => for {
        a <- m.ask(state)
        b <- f(a).ask(state)
      } yield b
    }

}

这个 Type Lambda 在 Scala 2.12 中不能被正确识别,所以我需要对不同的版本分离出不同的实现。

SBT 提供了内置的支持,我们可以在代码目录中建立不同版本的子目录:

 当我们用 ++ 选项指定版本时,sbt会根据编译器版本,将 scala-2.12 或 scala-2.13目录下的版本包含进去。

而 Intellij 也会根据 scalaVersion 选择对应的子版本目录。

对于 2.12 版本,我并不追求类型设计的效果,只要能够正常的解析字符串就可以。因此对于 2.12 的 Parsec 定义,我做了降级,首先将各种 Monad 方法,包含 <* 、>>= 等运算符的实现,都直接写进 Parsec 定义,其次也不追求实现一个支持 [E, T] 的完整的 toMonad 方法,而是定义一个支持 Parsec[Char, T] 类型的 toMonad。因为只需要支持 Parsec[Char, T],所以就可以视作一个单类型参数的简单泛型类。在这个前提下,2.12 和 2.13 版本的使用方法完全一致,所有测试都正常通过。

其实我还试验了 scala 2.11,这个版本似乎不支持 SAM(即 Java 8 随 Lambda 语法引进的那个单方法实现类型自动识别),也就是说,Scala 2.12/2.13共享的大部分方法,对于 2.11 需要重写,如果我加入 2.11 支持,就要在sbt里设定 2.11 包含一整个独立编译的代码结构。这个部分要如何组织才合理,我还没有想好。

最后,根据sbt文档,我应该可以通过 

sbt + publish

发布跨版本的项目。因为我需要向 sonatype 的开放源码库发布项目,所以需要调用 publishSigned 任务。

但是不知道是否 publishSigned 子任务还不支持 + ,还是我的 sbt 定义有问题,我最终是修改 scalaVersion,将 2.12 和 2.13 的版本各发布了一遍。


就在刚刚,经过社区同行的指点,我解决了 2.11 的问题。主要通过这样几个步骤:

首先,在sbt 的 crossVersions 中加入 2.11 

lazy val scala213 = "2.13.6"
lazy val scala212 = "2.12.15"
lazy val scala211 = "2.11.12"
lazy val supportedScalaVersions = List(scala213, scala212, scala211)

version := "0.7.4"
scalaVersion := scala213

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.12" % "test"
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3" % "test"

crossScalaVersions := supportedScalaVersions

然后,对 2.11 加入一个单独的编译选项,使其支持 SAM 。

def scalacOptionsVersion(scalaVersion: String): Seq[String] = {
  Seq(
    "-unchecked",
    "-deprecation",
    "-Xlint",
    "-Xfatal-warnings",
    "-Ywarn-dead-code",
    "-encoding", "UTF-8"
  ) ++(CrossVersion.partialVersion(scalaVersion) match {
    case Some((2, scalaMajor)) if scalaMajor == 11 => Seq("-Xexperimental")
    case _ => Nil
  })
}

val appSettings = Seq(
  scalacOptions := scalacOptionsVersion(scalaVersion.value)
)

这样,我们可以在 2.11 版本中使用 SAM,但是 2.11 的类型推导能力比后续版本要弱一些,其它版本中形如

val parser: Parsec[E, _] = s => for {

的SAM类型定义,要修改为

val parser: Parsec[E, _] = (s: State[E]) => for {

,为参数指定类型后,代码就可以正常编译了。

此外还有一个与 Parsec 的类型推导无关的兼容问题,scala 2.11 中没有 scala.collection.mutable.TreeMap 类型,因此我对TxtState 代码做了一些修改,让它使用 java.collection 。

package jaskell.parsec

import scala.collection.JavaConverters.asScalaSetConverter



class TxtState(val txt: String, val newLine:Char = '\n') extends CommonState[Char] {
  override val content: Seq[Char] = txt.toCharArray.toSeq
  val lines: java.util.SortedMap[scala.Int, scala.Int] = {
    val result = new java.util.TreeMap[scala.Int, scala.Int]();
    result.put(0, 0);
    for(index <- Range(0, txt.length)){
      val c = txt.charAt(index)
      if(c == newLine) {
        val lastIndex = result.lastKey
        result.put(lastIndex, index);
        if(index < txt.length - 1) {
          result.put(index+1, index+1)
        }
      }
    }
    result
  }
  def lineByIndex(index: scala.Int): scala.Int = {
    var i = 0
    for(idx:scala.Int <- lines.keySet().asScala){
      if(idx <= index && index <= lines.get(idx)) {
        return i
      }
      i += 1
    }
    -1
  }
}

object TxtState {
  def apply(txt: String, newLine: Char='\n'): TxtState = new TxtState(txt, newLine)
}

需要注意的是,代码中使用的 import scala.collection.JavaConverters 在 2.12 和 2.13 中已经 deprecated 。所以后续的版本中,scala 2.12 和 2.13 的实现,我仍然改回会使用  scala.collection.mutable.TreeMap 。目前的这个版本只是一个暂时存在的遗漏。但是它对暴露在外的功能接口没有影响。

最后,publish的问题也找到了,我在 + 后面多写了一个空格,正确的发布命令应该是

sbt +publishSigned

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ccat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值