Как я могу "сутенерствовать мою библиотеку" с помощью Scala в будущем?

Я использую неявные классы Scala для расширения объектов, с которыми я часто работаю. В качестве примера у меня есть метод, подобный DataFrame Spark DataFrame:

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

Но неявные defs не вызывается, если класс уже определяет один и тот же метод. Что произойдет, если я позже DataFrame#deduplicate на новую версию Spark, которая определяет DataFrame#deduplicate? Клиентский код будет тихо переключаться на новую реализацию, что может вызвать тонкие ошибки (или очевидные, что менее проблематично).

Используя отражение, я могу сбросить ошибку времени выполнения, если DataFrame уже определяет deduplicate прежде чем мой неявный определяет ее. Теоретически, если мой неявный метод конфликтует с существующим, я могу обнаружить его и переименовать мою неявную версию. Однако, как только я обновляю Spark, запускаю приложение и обнаруживаю проблему, слишком поздно использовать IDE для переименования старого метода, так как любые ссылки на df.deduplicate теперь относятся к df.deduplicate версии Spark. Я должен был бы вернуть свою версию Spark, переименовать метод через IDE, а затем снова выполнить обновление. Не конец света, но не большой рабочий процесс.

Есть ли лучший способ справиться с этим сценарием? Как можно безопасно использовать шаблон "pimp my library"?

Ответ 1

Вы можете добавить тест, который гарантирует, что определенные фрагменты кода не будут компилироваться в набор тестов DataFrameExtension. Может быть, что-то вроде этого:

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

Если он компилируется без вашего неявного преобразования, то это означает, что deduplicate метода была введена библиотекой Spark. В этом случае тест выходит из строя, и вы знаете, что вам нужно обновить свои импликации.

Ответ 2

Если метод расширения включен путем импорта, используйте -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
  }
}

Ответ 3

Решение для безопасного использования заключается в том, чтобы явно запросить расширенный кадр данных, чтобы минимизировать влияние, вы можете использовать неявный, чтобы иметь хороший синтаксис для конверсий (например, toJava/toScala и т.д.):

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

И тогда ваш призыв будет выглядеть:

myDf.asExtended
  .deduplicate
  .someOtherExtensionMethod
  .andMore

Таким образом, вы будете проверять будущее своими методами расширения без проверок времени выполнения/попыток лить/единичный тест (вы даже можете использовать myDf.ext это myDf.toExtended слишком длинное :))