Сегодня я затрону довольно интересную тему - имплициты. Они почти так же интересны, как и печально известны тем, что добавляют сложности кодовой базе и отлаживают адские истории (предупреждение о спойлере: как вы скоро узнаете - только если они используются неправильно).

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

Что такое имплициты

Со временем, когда вы привыкнете работать со Scala, вы в конечном итоге столкнетесь с аналогичным сообщением об ошибке компилятора:

ошибка: не удалось найти неявное значение для чтения параметра: JsonReads [Order]

Мне нравится проверять значение программного жаргона в словаре английского языка, чтобы действительно попытаться понять, почему что-то называется таким, как оно есть. Итак, давайте еще раз проверим, что означает неявное на человеческом языке. Словарь Коллинза говорит:

«То, что неявно выражается косвенным образом».

Итак, мы можем адаптировать его для соответствия сфере Scala:

Что-то неявное означает, что оно передается и используется косвенным образом, «за кулисами».

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

// inside com.taxwebsite.StaticValues
...
implicit val vat: Double = 0.19
...
// inside com.taxwebsite.TaxCalulator
...
def getPriceWithTax(amount: Int)(implicit vat: Double) = amount + amount * vat
...
// inside com.taxwebsite.PriceService
import com.taxwebsite.StaticValues.vat <- important chunk here!
...
def getFinalPrice(amount: Int) = getPriceWithTax(amount)
...

В приведенном выше примере кода у нас есть неявное значение vat, которое импортируется в область PriceService. Поскольку значение vat помечено как неявное, тогда нет необходимости явно сказать getPriceWithTax(amount)(vat), чтобы удовлетворить getPriceWithTax подпись - vat передается «за кулисами ».

Вот несколько способов воспроизвести ошибку, о которой я упоминал в самом начале:

  • Если убрать импорт из PriceService
  • … Или если мы не отметили val vat как неявное
  • … Или если val vat не был Double
  • … Или если getPriceWithTax не был Double

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

Как компилятор узнает, какое неявное значение и куда «вводить»?

Вы не можете написать «земля» без «искусства», но вы определенно можете написать «компилятор» без «магии».

Неявное разрешение

Давайте поговорим о том, где компилятор ищет имплициты, а также о том, на что он смотрит. Давайте возьмем за основу приведенный выше пример и будем думать, как компилятор. (*)

  1. Я звоню getFinalPrice
  2. Я вижу, что getFinalPrice вызывает getPriceWithTax, которому требуется неявное значение типа Double (обратите внимание, как компилятор не обращает внимания на имя значения. Только его тип)
  3. Есть ли в getFinalPrice какое-либо неявное значение Double? Нет.
  4. Есть ли в PriceService какое-либо неявное значение Double? Нет.
  5. Есть ли сопутствующий объект для PriceService? Нет.
  6. Есть ли в PriceService импорт, содержащий неявное значение Double? да.
  7. Хорошо, тогда я предполагаю, что это значение является неявным двойным значением, которое требуется getPriceWithTax. Я пропущу это безоговорочно «за кадром».

Что может быть неявным?

Это не просто val или var. Это тоже может быть implicit def, implicit class, implicit object.

Зачем использовать имплициты

Я намеренно пропускаю все подробности в конце раздела Что может быть подразумеваемым?. Мне кажется, что в большинстве материалов в Интернете есть множество примеров, но всегда упускаются два самых важных вывода, с которыми часто сталкиваются продвинутые новички. Они здесь:

  1. Неявные объекты (vals, классы, defs, objects) сами по себе очень мало используются в повседневном программировании. Существует очень строгий набор сценариев использования, в которые вы могли бы добавить их в свой код. В реальном мире вы не просто пишете неявный объект и не передаете его, как все онлайн-материалы (включая мой пример с налоговой службой выше!)
  2. Смыслы проявляются в гораздо более широкой картине на уровне абстракции шаблона дизайна. Мне на ум приходят два примера: реализация классов типов или неявных преобразований. Давайте теперь рассмотрим оба.

Типовые классы

Я пропущу здесь детали реализации. Это довольно сложная концепция дизайна, в основе которой лежит имплицит. Я не связываюсь ни с какими материалами просто потому, что классы типов слишком сложно понять, просто читая о них, и я не хочу чтобы вы разочаровались. Я расскажу о них однажды в статье. Короче говоря, шаблон класса типа позволяет (среди прочего) добавлять новый метод к существующему типу без изменения исходного кода этого типа.

Итак, используя класс типа, вы можете расширить typeString, чтобы иметь возможность вызывать на нем что-то необычное:

"Berlin".getCountry

Это известно как специальный полиморфизм. Классы типов широко используются в некоторых функциональных библиотеках Scala, наиболее известными из которых, вероятно, будут коты.

Интересный факт: классы типов и методы расширения должны быть намного проще в использовании в последней версии Scala, Scala 3. Признаюсь, я еще не пробовал это делать.

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

Еще одна концепция уровня шаблона проектирования, часто используемая для получения шаблона магнита. Вы можете представить это как ряд методов, которые компилятор пытается применить, когда неправильный тип аргумента передается существующей функции или классу. Они неявно преобразуют тип «за кулисами», чтобы соответствовать сигнатуре вызываемой функции. Интересным примером неявного преобразования может быть то, как можно передать Scala Int в методы Java, которые ожидают Java Integer.

Когда использовать имплициты

Итак, наконец, мы подошли к теме, которую, вероятно, ждет большинство читателей.

Настоящая жизнь.

У меня такое чувство, что большинство из вас на данном этапе понимают идею, лежащую в основе имплицитов, но не могут придумать ни одного варианта использования, в котором вы могли бы их использовать.

И это хорошо. Это именно то. Как я уже упоминал выше, вы не просто пишете неявный объект и не передаете его, как предлагают большинство материалов, охватывающих имплициты.

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

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

В реальной жизни вы сталкиваетесь с неявными последствиями, когда:

Необходимо удовлетворить зависимость библиотеки

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

  • Чтобы использовать большинство библиотек сериализатора JSON с настраиваемыми типами, вам необходимо создать неявный объект чтения / записи, который будет предоставлять метод расширения, такой как .toJson, для вашего настраиваемого типа, а затем импортировать это. Тогда вы можете позвонить: Person("Andrew").toJson
  • Чтобы использовать Scala Future, вам понадобится неявный контекст выполнения.
  • Scala's Duration выглядит как класс типа, не так ли?
// extension methods for Int imported here, you will find a lot of implicits in this package
import scala.concurrent.Duration._
1.second // "second" is an extension method on Int
10.millis // ...so is "millis"

Вы работаете с распределенной трассировкой

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

Вы реализуете шаблон класса типа или неявное преобразование

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

Выводы

Подведем итоги.

Что такое неявные?
Объекты, отмеченные ключевым словом «неявные». Это могут быть vals, vars, объекты, классы, defs. Если они указаны как зависимость другого объекта, они подбираются компилятором «за кулисами». Компилятор смотрит только на типы неявных значений, но не на имена.

Где они используются?
В основном в функциональных библиотеках или в хорошо известных шаблонах проектирования, таких как класс типа или неявные преобразования.

Когда мне следует использовать их в своем коде?
Избегайте любой ценой, если только вы не реализуете хорошо известный шаблон проектирования, который могут понять другие инженеры Scala или удовлетворяющий библиотечная зависимость. При неправильном использовании имплициты затруднят чтение, тестирование и сопровождение кодовой базы.
Короче говоря:
Если что-то еще нуждается в неявном - совершенно нормально, предоставьте его.
Если вы думаете, что что-то в написанном вами коде должно быть неявным: в 99 случаях из 100 - нет.

(*) конечно, это намного больше - для простоты я пропускаю большинство деталей в этом объяснении.