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

#scala #coding-style #implicit

#scala #coding-style #неявный

Вопрос:

Существуют ли какие-либо руководства по стилю, которые описывают, как писать код с использованием имплицитов Scala?

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

Комментарии:

1. Могло бы сделать это CW, если мы создадим ТАКИМ образом руководство по стилю

2. Существует ли какое-либо соглашение об именовании? Я видел такие вещи, как def foo2bar но мне действительно интересно, есть ли еще какой-либо консенсус…

3. @soc как насчет использования def FooToBar ? (с начальной прописной буквы) Таким образом, уменьшается вероятность скрытия другим неявным методом

4. @soc Лично я поддерживаю BippyIsBoppy соглашение; как используется в стандартной библиотеке: ByteIsIntegral , CharIsIntegral , FloatIsFractional и т.д.

Ответ №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-Library

Я больше всего использую неявные преобразования для конструкций в стиле 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 на месте, поскольку 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 ). Причина в том, что это делает классы-оболочки минимально навязчивыми. На самом деле вы не хотите знать об обертке; вам просто иногда нужна функциональность. Вы не можете полностью не замечать оболочку (например, вы можете исправить equals только в одном направлении, и иногда вам нужно вернуться к исходному типу). Но это часто позволяет вам писать код с минимальными затратами, что иногда как раз то, что нужно делать.

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

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

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

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

Комментарии:

1. Отличный ответ — мне нравится pair_is_foldable стиль именования; намного чище, чем то, что я делал 🙂

2. @oxbow_lakes — Я впечатлен, что вы смогли расшифровать это до того, как я исправил форматирование!

3. Scala допускает обратные ссылки, когда вы хотите поместить специальные символы в имя, чтобы затем вы могли использовать круглые скобки или формы обозначения и группировки, например implicit def '(T[S])to(String[S])' . Я нашел это полезным при использовании имплицитов для представления иерархической структуры. Смотрите мой ответ об эмуляции типа дизъюнкции. Здесь не могу показать обратные ссылки, поэтому вместо этого я использовал одинарные кавычки.

Ответ №2:

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

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

При неявном преобразовании из A в B , где это не тот случай, когда каждое A может рассматриваться как B , сделайте это с помощью 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"
  

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

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

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

Не используйте из-за лени!

Комментарии:

1. Не должно ли это быть type class , вместо typeclass ?

Ответ №3:

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

Итак, я собираюсь рискнуть здесь и назвать это шаблоном «pimp my type class«. Возможно, сообщество придумает что-нибудь получше.

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

Часть 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 — Расширение членов класса type с помощью операций

 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))
  }
}