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, там все красиво подсвечивается. А у меня уже голова не варит, я пойду. Всем спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/209850/


Комментарии

Добавить комментарий

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