Scala. Всем выйти из сумрака!

в 14:15, , рубрики: implicit, scala, новичкам, функциональное программирование, функциональщина, метки: , , ,

Scala. Всем выйти из сумрака!А сейчас нужно обязательно дунуть, потому что если не дунуть, то ничего не получится.
—Цитаты великих

И здравствуйте!

Сегодня мы поговорим о неявном в языке Scala. Кто еще не догадался — речь пойдет об implicit преобразованиях, параметрах, классах и иже с ними.Все новички, особенно любители питона с зеновским Explicit is better than Implicit, обычно впадают в кататонический ступор при виде подкапотной магии, творящейся в Scala. Весь компилятор и принципы в целом охватить за одну статью удастся вряд ли, но ведь дорогу осилит идущий?


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

А начнем мы с относительно простого раздела неявных преобразований. И жизненного примера.
Василий хочет себе автомобиль от производителя Рено. Вся семья долго копила деньги, но накопить всю сумму так и не смогли. Денег хватает разве что на новый ВАЗ. И тут резко хлоп! Рено покупает АвтоВАЗ. Вроде и производитель теперь нужный, да и денег хватает. Вот так вот неявно Вася теперь счастливый владелец иномарки.
Теперь попробуем это формализовать в виде кода:

Жизненный пример

case class Vasiliy(auto: Renault) {
   println("Vasiliy owns "+auto)
}

case class Renault(isRussian: Boolean = false)

case class VAZ(isRussian: Boolean = true)

object VAZ {
    implicit def vaz2renault(vaz: VAZ): Renault = Renault(vaz.isRussian) //вся магия здесь
}

object Family {
   def present = {
       Vasiliy(VAZ()) //подарим василию ВАЗ. Который внезапно Рено!
   }
}

В результате выполнения Family.present мы увидим строку Vasiliy owns Renault(true). Вот так Scala помогает обычным людям в этой непростой жизни!
Если привести более программисткий пример (что-то подобное использую у себя в проекте):

Безжизненный пример

case class PermissionsList(permissions: Set[String] = Set("UL"));

object PermissionsList {
    implicit def str2permissions(str: String) = PermissionsList(str.split(";").toSet)
    implicit def permissions2str(p: PermissionsList) = p.permissions.mkString(";")
}

//упрощенный
case class User(login: String, permissions: PermissionsList)

/* somewhere in a galaxy far far away  */
User(login = "Vasiliy", permissions = "UL;AL") // только ловкость рук и ничего более

Приведенный код позволяет неявно приводить строки к объекту прав доступа и обратно. Это может быть удобно при работе в том же вебе, когда нам достаточно только склеить на клиенте нужную строку вида "UL;AL" и отправить ее на сервер, где она уже будет в нужный момент преобразована в наш объект.
И вот мы подошли к важному пункту. Когда и при каких условиях наша тыква ВАЗ превратится в Рено, а строка в объект PermissionsList?

В подавляющем большинстве случаев вся магия Scala происходит в compile-time (язык-то строго типизирован). Местный компилятор — чрезвычайно умная и находчивая тварь. Как только мы пытаемся вызвать у инстанса класса VAZ метод exat(), который там и не существовал никогда, наш компилятор пускается во все тяжкие и варит мет ищет неявное преобразование VAZ'а во что-то, что умеет exat(). Иначе говоря, implicit def a2b(a: A): B.
Ищет он неявные преобразования:

  • В текущей области видимости (например, внутри текущего объекта)
  • В явных (import app.VAZ.vaz2renault)
  • или групповых импортах (import app.VAZ._)
  • В объекте-компаньоне преобразуемого

Кстати, помимо просто вызова несуществующего метода, компилятор попробует преобразовать объект в том случае, если мы попытаемся передать его в метод/функцию, которая требует параметр с другим типом данных. Это как раз из примера Василия и его семьи.


1.1 Implicit class

Начиная с версии 2.10 в Scala появились Implicit class'ы, которые позволяют удобно группировать расширения (довешивать методы) для любых существующих классов. Вот простенький пример:

object MySimpleHelper {
    implicit class StringExtended(str: String) {
        def sayIt = println(str)
        def sayItLouderBitch = println(str.toUpperCase +"!!!")
    }
}

Как видно из приведенных сырцов, мы имеем объявленный внутри объекта класс, который принимает единственный аргумент — строку. Эта строка дается нам на растерзание в методах класса. И терзается это дело элементарно:

import MySimpleHelper._

"oh gosh" sayIt
> oh gosh

"oh gosh" sayItLouderBitch
> OH GOSH!!!

Но и тут есть несколько ограничений, которые надо иметь ввиду:

  • Для implicit классов можно использовать только один явный аргумент конструктора, который, собственно и «расширяется» (про implicit параметры поговорим чуть позже)
  • Подобные классы могут быть объявлены только внутри объектов, трейтов, других классов
  • В области видимости объявления класса не могут существовать методы, свойства или объекты с тем же названием. Если у вас в, например, объекте есть свойство VAZ, то рядом не может сосуществовать implicit class VAZ

Ну а по факту, наш StringExtended будет преобразован компилятором в:

class StringExtended(str: String) {
   def sayIt = println(str)
   def sayItLouderBitch = println(str.toUpperCase +"!!!")
}

implicit def String2StringExtended(str: String): StringExtended = new StringExtended(str)

Знакомо, не так ли?


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

Как-то слишком все просто и вы уже заскучали? Самое время небольшого хардкора! Пошевелим мозгами и залезем в исходники скалы:

Неприветливый код

/**
* TraversableOnce.scala: minBy
* Итак, имеем метод, который позволяет найти минимум в коллекции, причем минимум будем определять мы сами, используя для этого функцию, возвращающую объект типа B для каждого элемента A коллекции. Собственно, эти объекты B и сравниваются между собой, а возвращается тот A, чей B меньше всех. Как-то так.
*/
def minBy[B](f: A => B)(implicit cmp: Ordering[B]): A = {
   //если коллекция пустая - что нам сравнивать?
    if (isEmpty)
      throw new UnsupportedOperationException("empty.minBy")

    //объявим пустые переменные нужных типов
    var minF: B = null.asInstanceOf[B]
    var minElem: A = null.asInstanceOf[A]
    var first = true //переменная для первой итерации

    //поехали по коллекции
    for (elem <- self) {
      //передаем в функцию элемент A, получаем некое B
      val fx = f(elem)
      if (first || cmp.lt(fx, minF)) {
        //если это наше первое сравнение - минимальный элемент будет первым же.
        //или же cmp.lt вернет true в том случае, если f: B < текущего минимума minF: B
        minElem = elem
        minF = fx
        first = false
      }
    }
    minElem
  }

Повтыкали, вроде все понятно.
Стоп, секунду. Но ведь мы используем minBy примерно так:

val cities = Seq(new City(population = 100000), new City(500000))
val smallCity = cities.minBy( city => city.population )

И никаких cmp: Ordering[B] (в данном случае B == Int) не передаем. Хотя вроде как код работает… Расслабься, парень. Это магия.
В импортированной области видимости, а конкретно в scala.math.Ordering существует

вот такой вот кусок кода

object Ordering extends LowPriorityOrderingImplicits {
...
    trait IntOrdering extends Ordering[Int] {
        def compare(x: Int, y: Int) =
          if (x < y) -1
          else if (x == y) 0
          else 1
    }

    implicit object Int extends IntOrdering
...
}

Обратим внимание на последнюю строку — существует неявный объект Int, который имеет в своем арсенале метод compare, имплементированный при наследовании Ordering[Int] трейтом IntOrdering. Собственно, этот объект и используется для сравнения, неявно передается в злополучный minBy.
Сильно упрощенный пример выглядит примерно так:

Приветливый код

implicit val myValue: Int = 5

object Jules {
    def doesHeLookLikeABitch(answer: String)(implicit times: Int) = {
       for(x <- 1 to times) println(answer )
    }
}

Jules.doesHeLookLikeABitch("WHAT?")

>WHAT?
>WHAT?
>WHAT?
>WHAT?
>WHAT?

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

  • В области видимости вызова метода должен существовать объект/значение, помеченный как implicit, причем существовать может только один параметр для одного типа данных. Иначе компилятор не поймет, что же нужно передать в метод.
  • Как вариант, компилятор пошарится в объекте-компаньоне нашего implicit T, если таковой существует, и дернет оттуда implicit val x: T. Но это уже совсем тяжелые наркотики, как по мне.

3. View/Context Bounds

Если ваш мозг уже оплавился — отдохните и выпейте кофе, а может чего покрепче. Я собираюсь поговорить о последней на сегодня неочевидности.
Допустим, что наш Василий, который ездит на новом автомобиле (тот самый, что умеет exat()) стал успешным человеком, программистом короче. И вот пишет Василий на Scala, и захотелось ему ЕЩЕ БОЛЬШЕ САХАРА АРРГХ. Мартин подумал и сказал — Окей. И ввел типы и ограничения по ним. Те самые def f[T](a: T)

3.1 View Bounds

Это ограничение при объявлении типа говорит компилятору о том, что истина неявное преобразование где-то рядом.

def f[A <% B](a: A) = a.bMethod

Т.е. в доступой области видимости присутствует неявное преобразование из A в B. В принципе, можно представить запись в следующем виде:

def f[A](a: A) (implicit a2b: A => B) = a.bMethod:

Близкий русскому человеку пример выглядит примерно так:

class Car {
   def exat() = println("POEXALI")
}
class VAZ
object VAZ {
    implicit def vaz2car(v: VAZ): Car = new Car()
}
def go[A <% Car](a: A) = a.exat()
go(new VAZ())

> POEXALI

Замечательно! Жизнь стала прекрасной, волосы выросли обратно, жена вернулась, ну и что там далее по списку.
Но Василий напросился, и Мартина была уже не остановить… Так появилось

3.2 Context Bounds

Это ограничение было введено в Scala 2.8, и, в отличии от View Bounds, отвечает не за неявные преобразования, а за неявные параметры, то есть

def f[A : B](a: A) = g(a) // где g принимает неявный параметр B[A]

Самым простым примером будет вот такая вот пара:

def f[A : Ordering](a: A, b: A) = if (implicitly[Ordering[A]].lt(a, b)) a else b

vs

def f[A](a: A, b: A)(implicit ord: Ordering[A]) = {
    import ord._
    if (a < b) a else b
}

Вообщем-то это отдаленный привет Хаскелю и его typeclass pattern'у.


Вместо послесловия

Дорога на этом не оканчивается, говорить о Scala можно долго и много. Но не все сразу, ведь главное — это понимание и желание понять. С желанием придет и осознание происходящего.
Ну а если вам после прочтения этой статьи не понятен код какого-то проекта, который использует неявные преобразования, советую закрыть блокнот и открыть нормальную IDE, там все красиво подсвечивается. А у меня уже голова не варит, я пойду. Всем спасибо за внимание.

Автор: ImLiar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js