Кодирование с использованием Scala в стиле
Существуют ли руководства по стилю, которые описывают, как писать код, используя импликации Scala?
Последствия действительно мощные, и поэтому ими можно легко злоупотреблять. Существуют ли общие рекомендации, в которых говорится, когда имплициты уместны, а при их использовании скрывает код?
3 ответа
Я не думаю, что пока есть стиль для всего сообщества. Я видел много соглашений. Я опишу мое и объясню, почему я его использую.
Именование
Я называю мои неявные преобразования одним из
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 на месте, так как 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, я заключаю его в маркерный класс (обычно с неявным преобразованием).
Я не верю, что я сталкивался с чем-то, поэтому давайте создадим это здесь! Некоторые практические правила:
Неявные преобразования
При неявном преобразовании из 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"
Неявные параметры
Используйте их для:
- предоставить экземпляры классов типов (например, скаляр)
- ввести что-то очевидное (например, предоставление
ExecutorService
какой-то рабочий вызов) - в качестве версии внедрения зависимости (например, распространение установки полей типа обслуживания в экземплярах)
Не используйте для лени!
Этот настолько малоизвестен, что ему еще предстоит дать имя (насколько мне известно), но он уже прочно утвердился как один из моих личных фаворитов.
Так что я собираюсь выйти на конечность и назовите ее "шаблонpimp my type class". Возможно, сообщество придумает что-то лучшее.
Это шаблон из трех частей, построенный полностью из последствий. Он также уже используется в стандартной библиотеке (начиная с версии 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 класса 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))
}
}