Многие начинающие и не очень Scala разработчики принимают
implicits как умеренно полезную возможность. Использование обычно
ограничивается передачей ExecutionContext
во
Future
. Другие же избегают неявного и считают
возможность вредной.
Код же вроде этого вообще многих пугает:
implicit def function(implicit argument: A): B
Но я считаю этот механизм важным преимуществом языка, давайте разберемся почему.
Кратко про implicits
В целом implicits это механизм автоматического дополнения кода при компиляции:
-
для неявных аргументов автоматически подставляется значение
-
для неявных преобразований значение автоматически оборачивается в вызов метода
Я не буду углубляться в эту тему, кому интересно посмотрите об этом видео от создателя языка. Вкратце один этот механизм используется в нескольких разных случаях (и в Scala 3 будет разделен на несколько отдельных возможностей):
-
Передача неявного контекста (как
ExecutionContext
) -
Неявные преобразования (не рекомендуется к использованию)
-
Методы-расширения (extension methods, синтаксический сахар по добавлению методов к существующим типам)
-
Классы типов (type classes) и неявный вывод (implicit resolution)
Классы типов и неявный вывод
Сами по себе классы тыпов не несут какой-то новизны или
революции это попросту реализация методов "для" типа, а не "в
самом" типе, это как разница между Comparable
&
Ordering
(Comparator
в Java):
Comparable
реализуется для добавления возможности
сравнения в ООП стиле:
class Person(age: Int) extends Comparable[Person] { override def compareTo(o: Person): Int = age compareTo o.age}def max[T <: Comparable[T]](xs: Iterable[T]): T = xs.reduce[T] { case (a, b) if (a compareTo b) < 0 => b case (a, _) => a}
Ordering
реализуется с той же целью, но отдельно от
самого типа:
case class Person(age: Int)implicit object ByAgeOrdering extends Ordering[Person] { override def compare(o1: Person, o2: Person): Int = o1.age compareTo o2.age}def max[T: Ordering](xs: Iterable[T]): T = xs.reduce[T] { case (a, b) if Ordering[T].lt(a, b) => b case (a, _) => a}// is syntactic sugar fordef max[T](xs: Iterable[T])(implicit evidence: Ordering[T]): T = ...
Вот и весь класс типов.
Интересным этот механизм делает неявный вывод. Вернемся к непонятному объявлению из начала статьи и добавим к нему разные комбинации:
implicit val value: A = ???implicit def definition: B = ???implicit def conversion(argument: C): D = ???implicit def function(implicit argument: E): F = ???
В чем разница? Исключая conversion
, который
является неявным преобразованием, почти ни в чем:
value
, definition
&
function
могут быть использованы чтобы подставить
значение для неявного аргумента нужного типа. Разница только в
способе вычисления: val
статичен, а
def
вычисляется каждый раз при постановке. Неявный
аргумент в такой функции работает как обычно требует неявного
значения в скоупе.
Неявная функция с неявным аргументом дает нам довольно интересные возможности выходит компилятор может не просто подставить неявное значение в нужное место, но и скомбинировать несколько вызовов для получения нужного значения, подобрав подходящие функции.
Рассмотрим пример мы можем описать порядок пар любых типов, для этого не нужно знать их заранее достаточно знать, что у них тоже есть порядок:
implicit def pairOrder[A: Ordering, B: Ordering]: Ordering[(A, B)] = { case ((a1, b1), (a2, b2)) if Ordering[A].equiv(a1, a2) => Ordering[B].compare(b1, b2) case ((a1, _), (a2, _)) => Ordering[A].compare(a1, a2)}// again, just syntactic sugar for:implicit def pairOrder[A, B](implicit a: Ordering[A], b: Ordering[B]): Ordering[(A, B)] = ...
Теперь если вдруг нам понадобится найти максимум среди сложных структур компилятор сам построит подходящий объект, комбинируя пользовательские и стандартные объявления:
val values = Seq( (Person(30), ("A", "A")), (Person(30), ("A", "B")), (Person(20), ("A", "C")))max(values) // => (Person(30),(A,B))
У нас был список типа Seq[(Person, (String,
String))]
и компилятор смог сам подобрать комбинацию функций
для построения Ordering
для этого типа:
max(values)( pairOrder( ByAgeOrdering, pairOrder(Ordering.String, Ordering.String) ))
Так неявный вывод позволяет описывать общие правила вывода и поручить компилятору свести эти правила вместе и получить конкретную имплементацию класса типов. Добавляя собственный тип или собственные правила не нужно описывать все с начала компилятор скомбинирует все сам, чтобы получить нужный объект.
А главное, если компилятору это не удастся вы получите ошибку компиляции, а не времени выполнения и сможете исправить проблему сразу на месте. Хотя конечно в бочке меда не без ложки дегтя если у компилятора не вышло, вы не знаете какого звена в цепочке не хватило дебажить такое не всегда просто.
Надеюсь неявное стало теперь немного более явным.