Кодирование с помощью Scala подразумевает стиль

Существуют ли руководства по стилям, описывающие, как писать код с помощью Scala implicits?

Implicits действительно мощные, и поэтому их можно легко злоупотреблять. Существуют ли какие-то общие рекомендации, которые говорят, когда имплициты подходят, а при использовании их скрывает код?

Ответы

Ответ 1

Я не думаю, что есть стиль для всего сообщества. Я видел множество конвенций. Я опишу свое и объясню, почему я его использую.

Нейминг

Я называю мои неявные преобразования одним из

implicit def whatwehave_to_whatwegenerate
implicit def whatwehave_whatitcando
implicit def whatwecandowith_whatwehave

Я не ожидаю, что они будут использоваться явно, поэтому я склоняюсь к довольно длинным именам. К сожалению, число в именах классов достаточно часто, поэтому соглашение whatwehave2whatwegenerate запутывается. Например: tuple22myclass - это Tuple2 или Tuple22, о котором вы говорите?

Если неявное преобразование определяется как аргументом, так и результатом преобразования, я всегда использую обозначение x_to_y для максимальной ясности. В противном случае я рассматриваю это имя как комментарий. Так, например, в

class FoldingPair[A,B](t2: (A,B)) {
  def fold[Z](f: (A,B) => Z) = f(t2._1, t2._2)
}
implicit def pair_is_foldable[A,B](t2: (A,B)) = new FoldingPair(t2)

Я использую как имя класса, так и неявное как своего рода комментарий о том, что является точкой кода, а именно добавить метод fold к парам (т.е. Tuple2).

Использование

Pimp-My-библиотека

Я использую неявные преобразования больше всего для конструкций стиля pimp-my-library. Я делаю это повсюду, где он добавляет недостающую функциональность или делает полученный код более понятным.

val v = Vector(Vector("This","is","2D" ...
val w = v.updated(2, v(2).updated(5, "Hi"))     // Messy!
val w = change(v)(2,5)("Hi")                    // Okay, better for a few uses
val w = v change (2,5) -> "Hi"                  // Arguably clearer, and...
val w = v change ((2,5) -> "Hi", (2,6) -> "!")) // extends naturally to this!

Теперь существует штраф за производительность, чтобы заплатить за неявные преобразования, поэтому я не пишу код в горячих точках таким образом. Но в остальном, я, скорее всего, буду использовать шаблон pimp-my-library вместо def, как только я перейду из нескольких применений в рассматриваемом коде.

Есть еще одно соображение, которое заключается в том, что инструменты не так надежны, показывая, где ваши неявные преобразования происходят из того места, откуда появляются ваши методы. Таким образом, если я пишу сложный код, и я ожидаю, что каждый, кто его использует или поддерживает, должен будет усердно изучить его, чтобы понять, что требуется, и как он работает, я - и это почти назад от типичная философия Java - более вероятно использование PML таким образом, чтобы сделать шаги более прозрачными для обученного пользователя. Комментарии будут предупреждать, что код необходимо понимать глубоко; как только вы глубоко поймете, эти изменения помогают, а не болят. Если, с другой стороны, код делает что-то относительно прямолинейное, я с большей вероятностью оставил бы defs in place, так как IDE помогут мне или другим быстро встать на скорость, если нам нужно внести изменения.

Избегать явных преобразований

Я стараюсь избегать явных преобразований. Вы, безусловно, можете написать

implicit def string_to_int(s: String) = s.toInt

но это ужасно опасно, даже если вы, кажется, нацарапаете все свои строки с помощью .toInt.

Основное исключение, которое я делаю для классов-оболочек. Предположим, например, вы хотите, чтобы метод принимал классы с предварительно вычисленным хэш-кодом. Я бы

class Hashed[A](private[Hashed] val a: A) {
  override def equals(o: Any) = a == o
  override def toString = a.toString
  override val hashCode = a.##
}
object Hashed {
  implicit def anything_to_hashed[A](a: A) = new Hashed(a)
  implicit def hashed_to_anything[A](h: Hashed[A]) = h.a
}

и вернемся к любому классу, который я начал с автоматически или, в худшем случае, путем добавления аннотации типа (например, x: String). Причина в том, что это делает классы-оболочки минимально навязчивыми. Вы действительно не хотите знать об обертке; вам просто нужна функциональность. Вы не можете полностью избежать заметок обертки (например, вы можете исправить только равные в одном направлении, а иногда вам нужно вернуться к исходному типу). Но это часто позволяет писать код с минимальным шумом, что иногда просто нужно делать.

Неявные параметры

Неявные параметры тревожно размножаются. Я использую значения по умолчанию, когда возможно, вместо этого. Но иногда вы не можете, особенно с общим кодом.

Если возможно, я пытаюсь сделать неявный параметр тем, что никакой другой метод никогда не использовал. Например, библиотека коллекций Scala имеет класс CanBuildFrom, который почти совершенно бесполезен, как что-либо иное, чем неявный параметр для методов коллекций. Таким образом, существует очень небольшая опасность непреднамеренных перекрестных помех.

Если это невозможно - например, если параметр необходимо передать нескольким различным методам, но это действительно отвлекает от того, что делает код (например, пытается выполнить регистрацию в середине арифметики), тогда вместо того, чтобы делать общий класс (например, String), это неявный val, я переношу его в класс маркера (обычно с неявным преобразованием).

Ответ 2

Я не верю, что мне что-то натолкнуло, поэтому давайте создадим его здесь! Некоторые эмпирические правила:

Неявные преобразования

При неявном преобразовании из A в B, где это не так, что каждый A можно рассматривать как B, делайте это с помощью pimping преобразования toX или чего-то подобного. Например:

val d = "20110513".toDate //YES
val d : Date = "20110513" //NO!

Не сойди с ума! Используйте для очень распространенную функциональность основной библиотеки, а не в каждом классе, чтобы сутенерствовать что-то ради этого!

val (duration, unit) = 5.seconds      //YES
val b = someRef.isContainedIn(aColl)  //NO!
aColl exists_? aPred                  //NO! - just use "exists"

Неявные параметры

Используйте их для:

  • предоставить typeclass экземпляры (например scalaz)
  • добавить что-то очевидное (например, предоставить ExecutorService для какого-либо рабочего вызова)
  • как версия инъекции зависимостей (например, распространять настройки полей типа обслуживания на экземплярах)

Не используйте для лень!

Ответ 3

Это настолько малоизвестно, что ему еще дано имя (насколько мне известно), но он уже прочно утвердился как один из моих личных фаворитов.

Итак, я собираюсь выйти на конечность здесь и назову его шаблоном сутенер моего типа. Возможно, сообщество придумает что-то лучшее.

Это трехчастный шаблон, полностью построенный на неявном. Он также уже используется в стандартной библиотеке (начиная с 2.9). Разъясняется здесь с помощью сильно вырезанного класса типа Numeric, который, как мы надеемся, будет знаком.

Часть 1 - Создайте класс типа

trait Numeric[T] {
   def plus(x: T, y: T): T
   def minus(x: T, y: T): T
   def times(x: T, y: T): T
   //...
}

implicit object ShortIsNumeric extends Numeric[Short] {
  def plus(x: Short, y: Short): Short = (x + y).toShort
  def minus(x: Short, y: Short): Short = (x - y).toShort
  def times(x: Short, y: Short): Short = (x * y).toShort
  //...
}

//...

Часть 2 - Добавить вложенный класс, обеспечивающий операции инфикс

trait Numeric[T] {
  // ...

  class Ops(lhs: T) {
    def +(rhs: T) = plus(lhs, rhs)
    def -(rhs: T) = minus(lhs, rhs)
    def *(rhs: T) = times(lhs, rhs)
    // ...
  }
}

Часть 3 - Элементы Pimp класса типа с операциями

implicit def infixNumericOps[T](x: T)(implicit num: Numeric[T]): Numeric[T]#Ops =
  new num.Ops(x)

Затем используйте его

def addAnyTwoNumbers[T: Numeric](x: T, y: T) = x + y

Полный код:

object PimpTypeClass {
  trait Numeric[T] {
    def plus(x: T, y: T): T
    def minus(x: T, y: T): T
    def times(x: T, y: T): T
    class Ops(lhs: T) {
      def +(rhs: T) = plus(lhs, rhs)
      def -(rhs: T) = minus(lhs, rhs)
      def *(rhs: T) = times(lhs, rhs)
    }
  }
  object Numeric {
    implicit object ShortIsNumeric extends Numeric[Short] {
      def plus(x: Short, y: Short): Short = (x + y).toShort
      def minus(x: Short, y: Short): Short = (x - y).toShort
      def times(x: Short, y: Short): Short = (x * y).toShort
    }
    implicit def infixNumericOps[T](x: T)(implicit num: Numeric[T]): Numeric[T]#Ops =
      new num.Ops(x)
    def addNumbers[T: Numeric](x: T, y: T) = x + y
  }
}

object PimpTest {
  import PimpTypeClass.Numeric._
  def main(args: Array[String]) {
    val x: Short = 1
    val y: Short = 2
    println(addNumbers(x, y))
  }
}

Ответ 4

Я не знаю исследований, но есть некоторые эмпирические правила:

  • Не используйте implicits, которые преобразуют базовые типы в базовые типы, например, String в int.

  • Не используйте implicits, где вы никогда их не ожидали.

Я надеюсь, что это поможет.