Автор статьи: Рустем Галиев

IBM Senior DevOps Engineer & Integration Architect. Официальный DevOps ментор и коуч в IBM

Привет, Хабр! У меня на поддержке есть проект на Scala и, соответственно, у меня была необходимость разобраться в Scala. Хочу рассказать про интересную концепцию в Scala, которая называется имлициты.

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

В этой статье мы погрузимся в мир implicit в Scala, исследуем его суть, применение и возможности. Мы рассмотрим, как implicit обеспечивает поддержку для реализации различных паттернов и шаблонов программирования, а также как его использование способствует созданию более элегантных и эффективных решений задач. Давайте углубимся в эту удивительную возможность Scala и узнаем, как использовать implicit для улучшения вашего кода.

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

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

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

Однако, использование implicit требует осторожности, так как слишком частое или неуместное применение может сделать код сложным для понимания и усложнить отладку.

Имплициты можно представить как неявное значение.

В контексте Scala, implicit представляет собой механизм, который позволяет компилятору находить и использовать значения неявно. Эти значения могут быть переменными, методами, классами или объектами, которые могут быть внедрены автоматически для удовлетворения определенных требований в коде.

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

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

Имплициты подобны Map[Class[A], A], где A — объект. Он привязан к области видимости и всегда под рукой; следовательно, это неявно. Имплициты предоставляют множество замечательных методов, которые мы можем использовать в Scala.

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

package com.xyzcorp;

object MyApp extends App {

   implicit val rate: Int = 100

   def calcPayment(hours:Int)(implicit n:Int) = hours * n

   println(calcPayment(30))

}

Скомпилируем

scalac -d target src/MyApp.scala

Запустим

Нам не обязательно принимать неявное значение; мы можем разместить что-то вручную, если хотим переопределить неявное значение. Обратите внимание, как в следующем примере мы добавляем второе значение 110:

package com.xyzcorp;

object MyApp extends App {

  implicit val rate: Int = 100

  def calcPayment(hours:Int)(implicit rate:Int) = hours * rate

  println(calcPayment(50)(110))
}

Однако у имплицитов бывают конфликты.

Компилятор ругается во время компиляции, если существуют две неявные привязки одного и того же типа. Стоит отметить, что Scala — это трюки времени компиляции для неявных функций. Одна из стратегий — заключить значение в тип, чтобы избежать конфликта. Ниже приведен результат того, что происходит, когда вы пытаетесь связать два значения одного типа. Обратите внимание, что и rate, и age являются целыми числами (int).

package com.xyzcorp;

object MyApp extends App {

  implicit val rate = 100

  implicit val age = 40

  def calcPayment(hours:Int)(implicit rate:Int) =

      hours * rate

  calcPayment(50)

}

Давайте разрешим конфликт.

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

Обратите внимание, что и rate, и age больше не являются строками, а являются их типами, Rate и Age соответственно. В методе CalcPayment нам требуется тип Rate, и поскольку у Age есть свой тип, никакой двусмысленности нет.

package com.xyzcorp;

object MyApp extends App {

  case class Rate(value:Int)

  case class Age(value:Int)

  implicit val rate: Rate = Rate(100)

  implicit val age: Age = Age(40)

  def calcPayment(hours:Int)(implicit rate:Rate) =

    hours * rate.value

  println(calcPayment(50))

}

Имплициты часто используются для связывания сервисов, которые требуют сервис определённого типа, и вам не обязательно явно внедрять их везде; в данном случае давайте рассмотрим Future[+T]. В Scala Future не может выполняться без ExecutionContext. Проблема заключается в том, что существует множество вызовов, которые требуют ExecutionContext. В следующем примере есть Future[+T], который выполняется без имплицита. Обратите внимание, насколько многословен этот код:

package com.xyzcorp;

object MyApp extends App {

  import scala.concurrent.

  import java.util.concurrent.Executors

  val executor = Executors.newFixedThreadPool(4) //Java

  val executionContext: ExecutionContext =

    ExecutionContext.fromExecutor(executor)

  val future = Future.apply {

    println(s"Thread-name: ${Thread.currentThread().getName}")

    Thread.sleep(3000)

    50 + 100

  }(executionContext)

  future

    .map(x => x * 100)(executionContext)

    .foreach(a => println(a))(executionContext)

  Thread.sleep(5000)

  System.exit(0)

}

Теперь мы можем переписать предыдущий шаг с использованием имплицита. Обратите внимание, что, сделав это, нам не нужно устанавливать ExecutionContext явно. Каждый раз, когда у нас есть метод, требующий ExecutionContext, мы используем executionContext, который привязан неявно. Сравните и проанализируйте различия между этим шагом и предыдущим, чтобы глубже понять, что происходит.

package com.xyzcorp;

object MyApp extends App {

  import scala.concurrent.

  import java.util.concurrent.Executors

  val executor = Executors.newFixedThreadPool(4) //Java

  implicit val executionContext: ExecutionContext =

    ExecutionContext.fromExecutor(executor)

  val future = Future.apply {

    println(s"Thread-name: ${Thread.currentThread().getName}")

    Thread.sleep(3000)

    50 + 100

  }

  future

    .map(x => x * 100)

    .foreach(a => println(a))

  //Let the future do it's job, System.exit required for KataCoda

  Thread.sleep(5000)

  System.exit(0)

}

Сегодня вы ознакомились с основами установки неявных значений. Вы узнали, что можно переопределять значения и теперь понимаете, что возможны конфликты. Теперь у вас также есть представление о том, как связывать сервисы вручную и неявно, чтобы сократить объем используемого кода.

Продолжим изучать имплициты в Scala на открытом уроке в OTUS. Записаться на урок можно на странице онлайн-курса "Scala разработчик".

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


  1. amishaa
    28.11.2023 18:02
    +4

    Стоило написать, что это всё касается второй скалы, в третьей скалы этот же механизм сделан иначе, через using/given.


  1. LedIndicator
    28.11.2023 18:02
    +2

    Про имплицитные функции тоже можно было упомянуть, не только про параметры.

    implicit def stringToInt(s: String): Int = augmentString(s).toInt
    
    ...
    
    def squareValue(myValue: Int): Int = myValue * myValue

    А потом можно вызвать

    val mySquare = squareValue("42")

    Строка "42" автоматически преобразуется в число.


  1. areful
    28.11.2023 18:02
    +1

    Для меня концепция так и осталась тяжеловесной добавкой в чистый и понятный язык. Многословность вещь субъективная. По мне явное добавление в вызов функции ещё одного параметра, который к тому же уже определён заранее, читаемость только улучшает. Неявные преобразования на первый взгляд вообще противоречат типобезопасному языку. С добавлением методов в уже существующие объекты можно было придумать какой-то явный механизм расширения. Хотя может быть так реализовано для гладкости, другого слова подобрать не могу, реализации этих концепций при синтаксического разборе.