Как работают имплиситы в Scala?

Как работают имплиситы в Scala

Программисты стремятся писать простой и понятный код. Чем меньше кода написано, тем меньше шансов, что в нём есть ошибка. Scala предлагает возможность писать ещё меньше кода и положиться на помощь компилятора. Это достигается за счёт неявных преобразований и неявных параметров. Но всё, что неявно, обычно вносит лишь непонимание. Давайте разбёремся, что скрывается за магией имплиситов в Scala.

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

Начнём с неявных преобразований (implicit conversions). Цель их применения можно понять из самого названия - трансформация данных одного типа в другой (строки в дату, массива в Option, мягкого в теплое). Если в области видимости присутствует неявная функция A => B, то компилятор может самостоятельно выполнить такое преобразование:

def call(str: String): Unit = println(str)

implicit def intToString(i: Int): String = i.toString

call(1)

Посмотрим, как scalac разбирается с этим кодом (тут и далее используется ключ -Xprint:typer для компилятора):

object ImplicitApp extends Object with App {
    def call(str: String): Unit = scala.Predef.println(str);
    implicit def intToString(i: Int): String = i.toString();
    ImplicitApp.this.call(ImplicitApp.this.intToString(1))
}

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

Такой вариант использования неявных преобразований наиболее неочевидный. Только IDE и часы отладки смогут подсказать, почему работает вызов метода с параметром не того типа. Не злоупотребляйте данным видом неявных преобразований.

Методы расширения

Более предсказуемыми преобразованиями являются методы расширения (extension methods). Они приходят на помощь, если мы хотим добавить новый метод в класс, доступа к исходным кодам которого у нас нет. Нужный функционал добавляется через новый неявный класс:

implicit class RichString(str: String) {
    def awesomeMethod(): Unit =
        println(s"awesomeMethod for $str")
}

"string".awesomeMethod()

Или же с помощью неявной функции, которая создает анонимный класс с необходимыми методами:

implicit def richString(str: String) = new {
    def awesomeMethod(): Unit =
        println("awesomeMethod")
}

"string".awesomeMethod()

Под капотом происходит следующее (на примере функции с анонимным классом):

object MethodExtension extends AnyRef with App {
    implicit def richString(str: String): AnyRef{def awesomeMethod(): Unit} = {
        final class $anon extends scala.AnyRef {
            def awesomeMethod(): Unit =
                scala.Predef.println("awesomeMethod")
        };

        new $anon()
    };

    MethodExtension.this.richString("string").awesomeMethod()
}

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

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

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

implicit val executor: Executor = (task: Task) => println(task.toString)

def run(task: Task)(implicit executor: Executor): Unit = executor.run(task)

run(new Task {})

Алгоритм действия такой же: компилятор находит в области видимости имплисит значение нужного типа и передаёт его вместо нас в функцию:

object ImplicitParameter extends AnyRef with App {
    private[this] val executor: Executor = 
        ((task: Task) => scala.Predef.println(task.toString()));
        
    implicit <stable> <accessor> def executor: Executor = 
      ImplicitParameter.this.executor;
  
    def run(task: Task)(implicit executor: Executor): Unit = executor.run(task);

    ImplicitParameter.this.run({
        final class $anon extends AnyRef with Task {};
        new $anon()
    })(ImplicitParameter.this.executor)
};

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

Type classes

Соединив вместе методы расширения и неявные параметры, получается подходящий инструмент для работы с классами типа. Класс типа (typeclass) - некая характеристика, описывающая, что умеет делать класс, какие операции с этим классом можно выполнять. Наиболее очевидный пример из стандартной библиотеки - трейт Ordering. Он сообщает о том, что объекты данного типа могут быть упорядочены. Создадим свой собственный typeclass и удобный API с помощью имплиситов:

trait Equal[A] {
    def equal(a1: A, a2: A): Boolean
}

object Equal {
    def apply[A](implicit instance: Equal[A]): Equal[A] = instance

    implicit class EqualSyntax[A](a: A) {
        def equal(that: A)(implicit e: Equal[A]): Boolean =
 	     e.equal(a, that)
    }
}

Equal можно использовать как напрямую (apply метод):

implicit val intEqual: Equal[Int] =
 (a1: Int, a2: Int) => a1 == a2

println(Equal[Int].equal(1, 2))

так и через метод расширения, который добавляет метод для сравнения в любой класс (при наличии соответствующих имплиситов):

import Equal.EqualSyntax

println(1 equal 2)

Так как с используемыми в последних двух примерах подходами мы уже познакомились, то в действиях компилятора никаких неожиданностей для нас не будет:

object TypeClass extends AnyRef with App {
    private[this] val intEqual: Equal[Int] = 
        ((a1: Int, a2: Int) => a1.==(a2));
    implicit <stable> <accessor> def intEqual: Equal[Int] = 
        TypeClass.this.intEqual;
    scala.Predef.println(Equal.apply[Int](TypeClass.this.intEqual).equal(1, 2)); // подстановка неявного параметра
  
    import Equal.EqualSyntax;
    scala.Predef.println(Equal.EqualSyntax[Int](1).equal(2)(TypeClass.this.intEqual)) // неявное преобразование в необходимый typeclass
}

Вместо заключения несколько истин от КО:

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

Исходный код примеров доступен на Github.

Связаться