Пробежавшись по предыдущим статьям на Хабре, тыц и тыц так и не удалось в быстром режиме понять, что делает неявность (implicit) в Scala. Попробуем разобраться вместе.


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

Например, мы могли бы написать функцию для преобразования из Float в Int(FloatToInt) и, вместо того, чтобы вызвать эту функцию явно, компилятор бы сделал это вместо нас неявно:

def double(value: Int) = value * 2
implicit def FloatToInt(value: Float):Int = value.toInt
println(double(2.5F))

Запутанно? Давайте обо всём по порядку.

Итак, сегодня мы рассмотрим две категории implicit, а именно:

  • Неявные параметры (implicit parameters, values). Они являются автоматически переданными компилятором значениями, объявленными через implicit.
  • Неявное преобразование (implicit conversion). При несовпадении типа ожидаемого параметра с входящим параметром компилятор ищет любой метод в области видимости, отмеченный implicit и с нужными в данной ситуации ожидаемым параметром и входящим параметром и автоматически его передаёт.

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


Неявный параметр — это параметр функции, которому предшествует ключевое слово implicit. Оно значит, что, если не передано никакого значения при вызове функции, то компилятор собственноручно будет искать неявный параметр и передаст его за нас.

Например, такая функция, принимающая на вход неявный параметр value и удваивающая его:

def double(implicit value: Int) = value * 2

Без неявного параметра упадёт с ошибкой:

def double(implicit value: Int) = value * 2
println(double) // error: could not find implicit value for parameter value

С переданным явно параметром сработает:

def double(implicit value: Int) = value * 2
println(double(3))  // 6

С неявным параметром это будет выглядеть так:

def double(implicit value: Int) = value * 2
implicit val multiplier = 2
println(double) // 4

Но нужно быть аккуратным:

def double(implicit value: Int) = value * 2
implicit val multiplier = 2
implicit val multiplier2 = 1 
println(double) // error: ambiguous implicit values

И напоследок пример с передачей в качестве неявного параметра def:

def double(implicit value: Int) = value * 2
val cappucinoLarge: Boolean = false
implicit def cappucinoPrice: Int = if (cappucinoLarge) 200 else 100
println(double) // 200

Т.е. в итоге у нас получится
double(cappucinoPrice())

Примечания по синтаксису:


def func1(implicit value1: Int)                          // value1 неявно
def func2(implicit value1: Int, value2: Int)             // value1 и value2 неявны
def func3(value1: Int, implicit value2: Int)             // ошибка компиляции
def func4(value1: Int)(implicit value2: Int)             // только value2 неявно
def func5(implicit value1: Int)(value2: Int)             // ошибка компиляции
def func6(implicit value1: Int)(implicit value2: Int)    // ошибка компиляции

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


Возвращаясь к примеру из Float в Int:

def double(value: Int) = value * 2
implicit def FloatToInt(value: Float):Int = value.toInt
println(double(2.5F))

При вызове double у нас происходит несовпадение типа ожидаемого параметра (Int) с входящим параметром (Float). Поэтому компилятор ищет любой метод в области видимости, отмеченный implicit и с нужными в данной ситуации ожидаемым параметром (Int) и входящим параметром (Float). Находит FloatToInt, производит преобразование и передает дальше нужное значение в double.

Теперь, надеюсь, стало понятнее. Всем успехов в освоении Scala!

Комментарии (7)


  1. akryukov
    11.05.2019 20:57
    +1

    Используя implicit аргументы функций можно наворотить крайне запутанный код на ровном месте. Один из тех случаев, когда "можно", но лучше не надо.
    С учетом этого, считаю недостатком, что в статье не описаны best-practices по использованию этой фичи языка.


    1. atamur
      12.05.2019 17:38

      Вот тут сам автор языка очень хорошо описывает основные случаи использования (осторожно, антимонгольский): https://youtu.be/br6035SKu-0


  1. JediPhilosopher
    12.05.2019 19:26

    Имплиситы — одна из вещей из-за которых я решил не продолжать свое знакомство со скалой.

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

    Экономия на количестве кода при этом грошевая, а удар по читабельности огромный.

    Неявные преобразования — тоже тот еще способ выстрелить себе в ногу, их полезность по сути только в том, чтобы можно было добавить один магический импорт и бесплатно получить конвертацию между джава и скала классами. Ведь своего богатого набора библиотек у скалы нет, приходится пользоваться джавовскими, а даже базовые классы (списки всякие и прочие популярные структуры данных) у них не совместимы. В итоге чтобы использовать джава-код надо либо вручную писать сотни бойлерплейта по конвертации одних коллекций в другие, либо вот использовать иплиситы как костыль.


    1. atamur
      13.05.2019 17:54

      Да, функционал одновременно и рискованный и мощный. Многие варианты использования имплиситов неуместны, а неявные преобразования и вовсе будут исключены из языка. С другой стороны этот механизм позволяет реализовать очень интересный механизм — автоматический вывод структур. Например если существует json формат для чисел и строк, и показано как строить форматы для списков и мап, то формат для Map[String, Seq[Int]] выводится автоматически и на стадии компиляции. При этом это не вшито в библиотеку json, а работает для любых типов.


    1. den_po
      13.05.2019 19:14

      их полезность по сути только в том, чтобы можно было добавить один магический импорт и бесплатно получить конвертацию между джава и скала классами

      Иногда хочется добавить метод к чужому классу. К примеру, штатно коллекции имеют методы min/max, но для пустых коллекций они бросают исключение. А с неявными преобразованиями нужные методы будто всегда жили в коллекциях.


      scala> implicit final class SeqMaxOption[T](val data: Seq[T]) extends AnyVal {
           |   def maxOption(implicit cmp: Ordering[T]): Option[T] = if (data.isEmpty) None else Some(data.max)
           |   def minOption(implicit cmp: Ordering[T]): Option[T] = if (data.isEmpty) None else Some(data.max)
           | }
      defined class SeqMaxOption
      
      scala> List[Int]().minOption
      res5: Option[Int] = None
      
      scala> List(1,2,3).minOption
      res6: Option[Int] = Some(3)


      1. den_po
        13.05.2019 20:05

        Ох, не оставил бы этот комментарий, не заметил бы, что minOption у меня с ошибкой =)


  1. sudoer
    14.05.2019 15:44

    Без неявного параметра упадёт с ошибкой:

    def double(implicit value: Int) = value * 2
    println(double) // error: could not find implicit value for parameter value

    Упадёт в рантайме или всё таки не скомпилируется?

    И по поводу синтаксиса: неужели нет способа сделать неявным только первый параметр?