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

    Подробнее
    Реклама
    Комментарии 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._.

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