Scala. Get out of the dusk!

    And now you need to blow, because if you do not blow, then nothing will work.
    - Great quotes

    And hello!

    Today we’ll talk about the implicit Scala language. Those who haven’t guessed yet will be talking about implicit transformations, parameters, classes and others like them. All newcomers, especially python lovers with Xenovian Explicit is better than Implicit , usually fall into a catatonic stupor when they see the engine hood magic working in Scala. It is unlikely that it will be possible to cover the entire compiler and the principles as a whole in one article, but will the road be overpowered?

    1. Implicit conversions


    And we will start with a relatively simple section of implicit transformations. And a life example.
    Vasily wants a car from the manufacturer Renault. The whole family was saving money for a long time, but they could not accumulate the entire amount. Only enough money for a new WHA. And then abruptly clap! Renault buys AvtoVAZ. It seems that the manufacturer is now needed, and there is enough money. And so, implicitly, Vasya is now the happy owner of a foreign car.
    Now we try to formalize this in the form of code:

    Life example
    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()) //подарим василию ВАЗ. Который внезапно Рено!
       }
    }
    

    As a result of execution, Family.presentwe will see a line Vasiliy owns Renault(true). This is how Scala helps ordinary people in this difficult life!
    If you give a more programmatic example (I use something similar in my project):

    Lifeless example
    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") // только ловкость рук и ничего более
    

    The above code allows you to implicitly cast strings to an access object and vice versa. This can be convenient when working on the same web, when we just need to glue the desired line of the form on the client "UL;AL"and send it to the server, where it will be converted to our object at the right time.
    And so we came to an important point . When and under what conditions will our VAZ pumpkin turn into Renault and the string into a PermissionsList object?

    In the vast majority of cases, all Scala magic happens in compile-time (the language is strongly typed). The local compiler is an extremely smart and resourceful creature. As soon as we try to call the exat () method on an instance of the VAZ class, which never existed there, our compiler starts up in all serious ways and cookslooking for an implicit conversion of VAZ into something that exat () can do. In other words, implicit def a2b(a: A): B.
    He is looking for implicit conversions:
    • In the current scope (for example, inside the current object)
    • In explicit ( import app.VAZ.vaz2renault)
    • or group imports ( import app.VAZ._)
    • In a companion object convertible

    By the way, in addition to just calling a nonexistent method, the compiler will try to convert the object if we try to pass it to a method / function that requires a parameter with a different data type. This is just from the example of Vasily and his family.

    1.1 Implicit class


    Starting with version 2.10, Implicit classes have appeared in Scala that allow conveniently grouping extensions (adding methods) for any existing classes. Here is a simple example:

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

    As you can see from the cited raws, we have a class declared inside the object that takes a single argument - a string. This line is given to us to be torn to pieces in class methods. And this thing is tormented simply:

    import MySimpleHelper._
    "oh gosh" sayIt
    > oh gosh
    "oh gosh" sayItLouderBitch
    > OH GOSH!!!
    

    But here there are several limitations that must be borne in mind:
    • For implicit classes, you can use only one explicit constructor argument, which, in fact, “expands” (we will talk about implicit parameters a bit later)
    • Such classes can only be declared inside objects, traits, other classes.
    • In the scope of a class declaration, methods, properties, or objects of the same name cannot exist. If you have a property in, for example, an object VAZ, it cannot coexist nearby.implicit class VAZ

    Well, in fact, ours StringExtendedwill be converted by the compiler to:

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

    Familiar, right?

    2. Implicit parameters


    Somehow everything is too simple and you are already bored? It's time for a little hardcore! Let's move our brains and climb into the source of the rock:

    Surly code
    /**
    * 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
      }
    

    Pushed, it seems everything is clear.
    Stop, second. But we use minBy something like this:
    val cities = Seq(new City(population = 100000), new City(500000))
    val smallCity = cities.minBy( city => city.population )
    

    And we do cmp: Ordering[B] (в данном случае B == Int)n’t transmit any. Although it seems like the code works ... Relax, man. It `s Magic.
    In the imported scope, specifically in scala.math.Orderingexists

    such a piece of code
    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
    ...
    }
    

    Let's pay attention to the last line - there is an implicit Int object that has in its arsenal a method compareimplemented when inheriting by Ordering[Int]trait IntOrdering. Actually, this object is used for comparison, implicitly passed to the ill-fated minBy.
    A highly simplified example looks something like this:

    Friendly code
    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?
    

    Of course, no one forbids us to transfer implicit parameters by handles. No, well, suddenly it will be needed.
    And again, again, restrictions, where without them.
    • An object / value marked as implicit must exist in the scope of the method call, and there can only be one parameter for one data type. Otherwise, the compiler will not understand what needs to be passed to the method.
    • Alternatively, the compiler will rummage around in our companion object implicit T, if one exists, and pulls from there implicit val x: T. But these are already quite hard drugs, as for me.


    3. View / Context Bounds


    If your brain is already melted, take a break and drink coffee, or maybe something stronger. I'm going to talk about the latest non-obviousness for today.
    Let's say that our Vasily, who drives a new car (the one that knows how exat()), has become a successful person, a programmer in short. And so Vasily wrote on Scala, and he wanted another MORE SUGAR ARRHH. Martin thought and said - Okay. And introduced types and restrictions on them. Those samedef f[T](a: T)

    3.1 View Bounds

    This restriction when declaring a type tells the compiler that the true implicit conversion is somewhere nearby.

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

    Those. in the available scope there is an implicit conversion from Ato B. In principle, you can imagine the entry in the following form:

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

    A close example to a Russian person looks something like this:

    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
    

    Wonderful! Life has become beautiful, hair has grown back, my wife has returned, so what next on the list.
    But Vasily asked for it, and Martin could no longer be stopped ... So it appeared

    3.2 Context Bounds

    This restriction was introduced in Scala 2.8, and, unlike View Bounds, it is not responsible for implicit conversions , but for implicit parameters , i.e.

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

    The simplest example would be such a pair:

    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
    }
    

    Actually, this is a distant hello to Haskell and his typeclass pattern.

    Instead of an afterword


    The road does not end there; you can talk about Scala for a long time and a lot. But not all at once, because the main thing is understanding and the desire to understand. With desire comes awareness of what is happening.
    Well, if after reading this article you don’t understand the code of some project that uses implicit conversions, I advise you to close the notebook and open the normal IDE, everything is beautifully highlighted there. And my head doesn’t boil, I’ll go. Thank you all for your attention.

    Also popular now: