【问题标题】:How can I "pimp my library" with Scala in a future-proof way?如何以一种面向未来的方式使用 Scala “拉皮条我的库”?
【发布时间】:2018-10-24 04:02:12
【问题描述】:

我使用 Scala 隐式类来扩展我经常使用的对象。例如,我有一个类似于 Spark DataFrame 上定义的方法:

implicit class DataFrameExtensions(df: DataFrame) {
  def deduplicate: Boolean = 
    df.groupBy(df.columns.map(col): _*).count
}

但是如果类已经定义了相同的方法,则不会调用隐式定义。如果我稍后升级到定义了DataFrame#deduplicate 方法的新版本的 Spark,会发生什么?客户端代码将静默切换到新的实现,这可能会导致细微的错误(或明显的错误,问题较少)。

使用反射,如果 DataFrame 在我的隐式定义之前已经定义了 deduplicate,我可以抛出 runtime 错误。然后,理论上,如果我的隐式方法与现有方法冲突,我可以检测到它并重命名我的隐式版本。但是,一旦我升级 Spark、运行应用程序并检测到问题,使用 IDE 重命名旧方法为时已晚,因为任何对 df.deduplicate 的引用现在都指的是原生 Spark 版本。我必须恢复我的 Spark 版本,通过 IDE 重命名方法,然后再次升级。不是世界末日,但不是一个伟大的工作流程。

有没有更好的方法来处理这种情况?如何安全地使用“pimp my library”模式?

【问题讨论】:

  • 我相信最安全(也是最简单)的做法是使用简单的函数而不是定义隐式类。 def deduplicate(df: DataFrame): Boolean = df.groupBy(df.columns.map(col): _*).count
  • 不理想,但您可以使用不太可能引入的名称,例如deduplicate_ 或任何其他约定。
  • 这似乎是fragile base class problem 的变体,但基于隐式的丰富不会对所涉及的类强加任何显式关系。所以编译器看不出问题。
  • 有点元和题外话,但有没有人觉得我们为 Scala 隐含三个不同的不相关标签感到奇怪?
  • @AndreyTyukin 使用 Scala 表达自己的方式总是太多。

标签: scala implicit implicits scala-implicits


【解决方案1】:

如果通过导入启用了扩展方法,则使用-Xlint 表示不再使用导入:

//class C
class C { def x = 17 }

trait T {
  import Extras._
  def f = new C().x
}

object Extras {
  implicit class X(val c: C) {
    def x = 42
  }
}

另一种观点,证据必须在-Xlint -Xfatal-warnings下使用:

//class C[A]
class C[A] { def x = 17 }

trait T {
  import Mine.ev
  val c = new C[Mine]
  def f = c.x
}

trait Mine
object Mine {
  implicit class X[A](val c: C[A]) {
    def x(implicit @deprecated("unused","") ev: Mine) = 42
  }
  implicit val ev: Mine = null
}

object Test {
  def main(args: Array[String]): Unit = println {
    val t = new T {}
    t.f
  }
}

【讨论】:

  • 这很酷。我对依赖特定的编译器标志来提醒我这个问题有点怀疑,因为这些可能很容易被不知情的第三方更改。但似乎是一个很好的纵深防御层。
【解决方案2】:

安全地做到这一点的解决方案是显式请求扩展数据帧,为了尽量减少影响,您可以使用隐式来获得一个很好的转换语法(如 toJava/toScala 等):

implicit class DataFrameExtSyntax(df: DataFrame) { 
 def toExtended: DataFrameExtensions = DataFrameExtensions(df)
}

然后您的调用将如下所示:

myDf.asExtended
  .deduplicate
  .someOtherExtensionMethod
  .andMore

这样您就可以在没有运行时检查/linting/单元测试技巧的情况下对扩展方法进行面向未来的验证 (你甚至可以使用myDf.extmyDf.toExtended 太长了:)

【讨论】:

  • 但是toExtended 不会遇到同样的问题吗?对于toJava,这个问题不存在(因为扩展方法和冲突的方法都必须来自同一个库),而toScala 可能会遇到这个问题,但这不太可能,因为名称是“命名空间”——这似乎是一个有问题的模式。
  • 可能,是的,你甚至可以选择一个更奇怪的名字......但是,为了更加安全,我会添加“shouldNot compile”测试
  • 我会认为 linting 是一种技巧,但你让我选择了一个替代方案,如果使用隐式,则表明你的 API 已被明确使用。当然,您的解决方案是经典的建议,但@Blaisorblade 是尖锐的、双关语的。
  • 我对这个解决方案并不感兴趣,因为它略微降低了可读性并引入了记住我的哪些数据框方法是“扩展”的,哪些是本机的,但我认为这是最面向未来的计划。其他答案对于 detecting 冲突有很好的答案,但是除了恢复 Spark 版本和更改名称之外,一旦检测到它就没有办法修复它。太糟糕了,没有 Scala method_missing ...
  • 动态特征 (scala-lang.org/api/2.12.4/scala/Dynamic.html) 的行为类似于“缺少方法”
【解决方案3】:

您可以将test that ensures that certain code snippets do not compile 添加到DataFrameExtension 的测试套件中。也许是这样的:

"(???: DataFrame).deduplicate" shouldNot compile

如果它在没有你的隐式转换的情况下编译,那么这意味着方法 deduplicate 已被 Spark 库引入。在这种情况下,测试失败了,你知道你必须更新你的隐式。

【讨论】:

  • 聪明。也许太多了。 ;-)
  • @stefanobaghino 这是一行 以某种方式测试您的嵌入式 DSL,所以...
  • 我的观点是,对于示例中的简单事情,可能建立在外部库上的嵌入式 DSL 是对语言功能的滥用。您已经找到了一种巧妙而简洁的方法来轻松发现问题,但例如如何解决这些错误等问题仍然悬而未决。重命名是一个可行的选择吗?我们可以安全地破坏 API 吗?我的观点不是反对你的解决方案,而是赞成不处理这个问题。我提出了一个关于问题本身的观点,从你处理所提出问题的绝妙方法可以看出。顺便说一句,+1。
  • @stefanobaghino 正如 Joe Pallas 上面所说,使用 pimp-my-library 模式定义 eDSL 引入了一个脆弱的基类问题的版本。如果您决定对别人的库进行拉皮条,那么您将依赖于受别人控制的脆弱基类,因此您至少应该监视对此类的更改是否会破坏您的代码。另一种方法是不使用 pimp-my-library 模式,但这不是问题。我不确定您所说的“嵌入式 DSL ......外部库......滥用权力”是什么意思。它是 ScalaTest,Scala 的事实上的标准测试框架。
  • 正如我所提到的,我在这个问题上提出了不同的观点。
猜你喜欢
  • 2012-10-24
  • 1970-01-01
  • 2014-11-17
  • 2015-07-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-27
  • 2014-07-06
  • 2013-04-21
相关资源
最近更新 更多