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