Пользователь
0,0
рейтинг
6 февраля 2014 в 18:15

Разработка → 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
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +1
    В IntelliJ IDEA можно увидеть все implicit преобразования, которые применяются к токену — наводим на него фокус и Ctrl + Shift + P. Плюс он подчеркивается тоненькой линией.
    • +1
      afair ctrl shift q для преобразований, ctrl shift p для параметров
      • 0
        Да, все так.
        Для мака это соответственно Ctrl + Q и Cmd + Shift + P
  • +1
    За стиль изложения — браво. Дочитал до конца :)
    • +1
      Благодарствую, рахат лукум моего сердца ;)
  • 0
    Определенно, стиль хорош! Продолжайте в том же духе.

    Удивительно, что не было сравнения implicit class с extension methods из C#.
    • 0
      К сожалению с сисярпом знаком лишь понаслышке
      • 0
        Очень похоже на implicit class StringExtended(val str: String) extemds AnyVal только к интерфейсу привести не позволяют.

        public static class StringExtended
        {
            public static void SayIt(this String str)
            {
                Console.WriteLine(str);
            }
        
            public static void SayItLouderBitch(this String str)
            {
                Console.WriteLine(str.ToUpper() + "!!!");
            }
        }   
        

  • +1
    По-моему язык, в котором код делает не то, что написано, эффектно бы смотрелся в презентации Акопяна или Кио. Scala со всей своей подкапотной магией, стремится стать сборником всевозможных заклинаний. Куча неявного, скрытого, магического и эзотерического. Когда кругом одна магия начинаешь чувствовать себя Гарри Поттером. Я предпочитаю принцип «less magic, more logic», который со Scala не совместим.
    • +4
      Это утверждение справедливо только в том случае, если использовать инструмент в слепую, не пытаясь в нем разобраться. Сюда же можно приписать и C/++, Java и многие другие.
      Скала — сложный язык, с относительно высоким порогом вхождения.
      Для большинства старушек компьютер есть шайтан-коробка и вообще от сатаны, срочно поставить кактус рядом. И что ж теперь, использовать печатные машинки и голубиную почту, просто потому что просто?
      • –3
        Вопрос, а стоит ли овчинка выделки? Насколько эффективность разработки на Scala покрывает ее сложность? Если мы возьмем полный цикл разработки проекта в команде, окажется ли он менее затратным, чем, скажем на Java? Я очень сомневаюсь. Методы решения поставленной задачи сильно не отличаются. Да, Scala эффективней тем, что меньше приходится писать. Но написание кода — это лишь небольшой процент от всего объема работы. Возможно, выигрываешь в написании — проигрываешь в отладке и поддержке.

        P.S. меня чисто эстетически воротит от Scala. Ее могли сделать только люди с напрочь отбитым эстетическим вкусом. При всей ее сложности синтаксис можно было бы сделять в сто раз приятнее, избегая засилия закорючек. В скале засилие сахара, искусственных конструкций, неявных преобразований и соглашений по умолчанию превышает все мыслимые и немыслимые пределы. Такое впечатление, что основная идея Одерски: «а давайте включим в язык сразу все, что есть в других языках, и это сделает его универсальным на все случаи жизни». Красота как раз в минимализме.
        • 0
          Про Java и крупные проекты… Я работал с крупными проектами на C#, где синтаксис не столь убог, как у Java, но и там отсутствие удобной работы с immutable объектами приводит к крайне плачевным результатам — система становится запутанной и непрозрачной.
          В Java с этим еще хуже, там даже вменяемого интерфейса для неизменяемых коллекций нет.

          А убийственным становится преимущество scala при работе с многопоточностью. Многопоточность в Java — боль и страдание макароны, непрозрачность и риск ошибок.

          По поводу закорючек: у меня есть 2 файла на scala: хотите пари? Один 400 строк без закорючек и логики, просто определение объектной модели. Другой на 100-150 строк с кучей «закорючек».
          Сможете переписать их на Java чтоб стало понятнее без закорючек? Готов не учитывать переписывание объектной модели, только переписывание файлика на 100-150 строк. Код написан когда я меньше года как узнал о скале. С вашим опытом Java у вас огромная фора.

          После этого пари я готов с вами обсуждать «закорючки» в scala.
        • +1
          если у вас в команде ленивые студенты-второкурсники, которые год назад узнали о существовании плюсов, а в школе максимум на бейсике процедурки писали, то конечно проект на скале не взлетит.

          а опус про эстетический вкус — чистейший воды субъективизм, который даже комментировать не хочется.
    • 0
      В очень многих язык есть возможность «расширить» класс, внести в него доп. методы. Scala тут не особо выделилась. Выше я привел аналог на C#.

      Плюс добавили некоторое обобщение над зарекомендовавшим себя в haskell механизмом typeclass.

      На любом языке можно писать невнятно. Если же соблюдать конвенции то все понятно и очевидно. Например неявные преобразования в чистом виде (а не в виде implicit class) приведут к предупреждению при компиляции — их вообще рекомендуется использовать только в крайнем случае.

      К слову о магии: я не пользуюсь IDE для scala обычно. Это не мешает мне абсолютно точно понимать что происходит.
      • 0
        Таки vim? Я знал, знал!
        • 0
          Иногда vim, но чаще Kate.
    • –1
      Ну вас же никто не заставляет это использовать. Это для разработчиков библиотек, а не для обычных программистов. Красивые выразительные DSL-и это очень клёво.

      PS последний пример в статье не понял, честно говоря. На свежую голову ещё перечитаю. За остальное спасибо, в принципе всё знал, но повторить не мешает.
      • 0
        Context bounds — сахарок в избавлении от implicit аргумента и реверанс в сторону хаскеля с возможностью простого неявного расширения типов.
        скидыщ и скидыщ

        Но лучше выспаться, да
  • +1
    Спасибо, очень здорово! Во вторник начал знакомиться со scala, и если бы прочитал это раньше, понял бы быстрее.
  • 0
    Самое главное в implicit'ах — не переборщить с их количеством, иначе весь код будет действительно как одна сплошная магия выглядеть.
    • 0
      достаточно вести разработку исходя из определенных соглашений, внутри команды например.

      но как было сказано выше — implicit полностью себя раскрывает как мощнейший инструмент скорее в либах и DSL
  • 0
    В scala — новичек, хотелось бы комментариев опытных коллег:
    обычно натыкаешься на implicit как раз когда начинаешь пользовтаься либами
    идешь в мануал — там пример import xxx._
    если не работает то еще втыкаешь import.yyy._
    Т.е что в java сильно не рекомендуется import xxx.* в скале это норма
    Понять что у тебя из либы при этом неявно в код попало- достаточно сложное занятие.
    В результате, если библиотека написана грамотно- то заработает, но остнется магией
    А вот со spray-routing повезло меньше- там мои save и get начали пересекатся с его внутренностями.
    • 0
      Со spray-routing — это давно известное и многими расценивается как баг. Может и поправят.
      Если либа написана хорошо, то зачастую не требуются wildcard импорты для implicit значений (растащены по компаньонам), либо создан отдельный объект для имплиситов с уникальными именами.

      А ошибку при проектировании библиотеки можно совершить и без имплиситов.

      Кстати, даже в случае с wildcard импортами в хороших библиотеках можно ограничивать ипортируемое. Например в scalaz можно импортировать все и сразу: import scalaz._, Scalaz._, но рекомендуют импортировать то, что нужно. Если нужны инструменты для работы с Option: import scalaz.syntax.std.option._.

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