—Цитаты великих
И здравствуйте!
Сегодня мы поговорим о неявном в языке 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/
Добавить комментарий