📜 ⬆️ ⬇️

How to use implicits in Scala and keep sanity

image


Scala is rich in expressive means, for which experienced programmers in classical OOP languages ​​dislike it. Implicit parameters and transformations are one of the most controversial features of the language. The word "implicit" is already hinting at something unclear and confusing. Nevertheless, if you make friends with them, implicits open up wide possibilities: from reducing the amount of code to the ability to do many checks in compile-time.


I want to share my experience in working with them and talk about what so far the official documentation and developer blogs are silent about. If you are already familiar with Scala, tried to use implicit parameters, but still have some difficulties when working with them, or at least heard about them, then this post may be of interest to you.



Content:



The implicit keyword refers to three concepts in Scala: implicit parameters, implicit conversions, and implicit classes.


Implicit parameters


Implicit parameters are parameters that can be automatically transferred to the function from the context of its call. To do this, it must be uniquely identified and labeled with the key word implicit variables of the corresponding types.


def printContext(implicit ctx: Context) = println(ctx.name) implicit val ctx = Context("Hello world") printContext 

Displays:


 Hello world 

In the printContext method, we implicitly get a variable of type Context and print the contents of its name field. Not scary yet.


The implicit parameter resolution mechanism supports generic types.


 case class Context[T](message: String) def printContextAwared[T](x: T)(implicit ctx: Context[T]) = println(s"${ctx.message}: $x") implicit val ctxInt = Context[Int]("This is Integer") implicit val ctxStr = Context[String]("This is String") printContextAwared(1) printContextAwared("string") 

Displays:


 This is Integer: 1 This is String: string 

This code is equivalent to as if we explicitly passed ctxInt parameters in the first case and ctxString parameters in the second to the printContextAwared method.


 printContextAwared(1)(ctxInt) printContextAwared("string")(ctxStr) 

Interestingly, implicit parameters do not have to be fields, they can be methods.


 implicit def dateTime: LocalDateTime = LocalDateTime.now() def printCurrentDateTime(implicit dt: LocalDateTime) = println(dt.toString) printCurrentDateTime Thread.sleep(1000) printCurrentDateTime 

Displays:


 2017-05-27T16:30:49.332 2017-05-27T16:30:50.476 

Moreover, implicit function parameters can, in turn, take implicit parameters.


 implicit def dateTime(implicit zone: ZoneId): ZonedDateTime = ZonedDateTime.now(zone) def printCurrentDateTime(implicit dt: ZonedDateTime) = println(dt.toString) implicit val utc = ZoneOffset.UTC printCurrentDateTime 

Displays:


 2017-05-28T07:07:27.322Z 

Implicit conversions


Implicit conversions allow you to automatically convert values ​​of one type to another.
To set an implicit conversion, you need to define a function from one explicit argument and mark it with the keyword implicit.


 case class A(i: Int) case class B(i: Int) implicit def aToB(a: A): B = B(ai) val a = A(1) val b: B = a println(b) 

Displays:


 B(1) 

All that is true for implicit parameter functions is also true for implicit conversions: generalized types are supported, there must be only one explicit, but there can be as many implicit parameters, etc.


 case class A(i: Int) case class B(i: Int) case class PrintContext[T](t: String) implicit def aToB(a: A): B = B(ai) implicit val cContext: PrintContext[B] = PrintContext("The value of type B is") def printContextAwared[T](t: T)(implicit ctx: PrintContext[T]): Unit = println(s"${ctx.t}: $t") val a = A(1) printContextAwared[B](a) 

Restrictions
Scala does not allow the use of several implicit conversions in a row, thus the code:


 case class A(i: Int) case class B(i: Int) case class C(i: Int) implicit def aToB(a: A): B = B(ai) implicit def bToC(b: B): C = C(bi) val a = A(1) val c: C = a 

Will not compile.
However, as we have already seen, Scala does not prohibit the search for implicit parameters along the chain, so that we can fix this code as follows:


 case class A(i: Int) case class B(i: Int) case class C(i: Int) implicit def aToB(a: A): B = B(ai) implicit def bToC[T](t: T)(implicit tToB: T => B): C = C(ti) val a = A(1) val c: C = a 

It is worth noting that if a function accepts a value implicitly, then in its body it will be visible as an implicit value or a transformation. In the previous example, to declare the method bToC, tToB is an implicit parameter and, at the same time, it already works as an implicit conversion inside the method.


Implicit classes


The implicit keyword before class declaration is a more compact form of writing an implicit conversion of the value of a constructor argument to a given class.


 implicit class ReachInt(self: Int) { def fib: Int = self match { case 0 | 1 => 1 case i => (i - 1).fib + (i - 2).fib } } println(5.fib) 

Displays:


 5 

It may seem that implicit classes are just a way to add a functional to a class, but in reality this concept is somewhat broader.


 sealed trait Animal case object Dog extends Animal case object Bear extends Animal case object Cow extends Animal case class Habitat[A <: Animal](name: String) implicit val dogHabitat = Habitat[Dog.type]("House") implicit val bearHabitat = Habitat[Bear.type]("Forest") implicit class AnimalOps[A <: Animal](animal: A) { def getHabitat(implicit habitat: Habitat[A]): Habitat[A] = habitat } println(Dog.getHabitat) println(Bear.getHabitat) // : //println(Cow.getHabitat) 

Displays:


 Habitat(House) Habitat(Forest) 

Here in the implicit AnimalOps class, we declare that the value type to which it will be applied will be visible to us as A, then in the getHabitat method we require the implicit parameter Habitat [A]. In its absence, as in the line with Cow, we get a compilation error.


Without the help of implicit classes, F-bounded polymorphism could help us achieve the same effect:


 sealed trait Animal[A <: Animal[A]] { self: A => def getHabitat(implicit habitat: Habitat[A]): Habitat[A] = habitat } trait Dog extends Animal[Dog] trait Bear extends Animal[Bear] trait Cow extends Animal[Cow] case object Dog extends Dog case object Bear extends Bear case object Cow extends Cow case class Habitat[A <: Animal[A]](name: String) implicit val dogHabitat = Habitat[Dog]("House") implicit val bearHabitat = Habitat[Bear]("Forest") println(Dog.getHabitat) println(Bear.getHabitat) 

As can be seen, in this case, the type of Animal significantly complicated the announcement, an additional recursive parameter A appeared, which plays an exclusively service role. This is confusing.


Chains of implicit parameters


This question is addressed in the official FAQ: http://docs.scala-lang.org/tutorials/FAQ/chaining-implicits.html .
As I said in the section on implicit conversions , the compiler does not know how to recursively use implicit conversions. However, it supports the recursive resolution of implicit parameters.


The example below adds, for those types for which the corresponding time -classes are implicitly defined, the describe method, which will return their description on a certain similarity of the human language (as we know, it’s impossible to determine the exact type in JVM runtime, so we define it in compile -time):


 sealed trait Description[T] { def name: String } case class ContainerDescr[P, M[_]](name: String) (implicit childDescr: Description[P]) extends Description[M[P]] { override def toString: String = s"$name of $childDescr" } case class AtomDescr[P](name: String) extends Description[P] { override def toString: String = name } implicit class Describable[T](value: T)(implicit descr: Description[T]) { def describe: String = descr.toString } implicit def listDescr[P](implicit childDescr: Description[P]): Description[List[P]] = ContainerDescr[P, List]("List") implicit def arrayDescr[P](implicit childDescr: Description[P]): Description[Array[P]] = ContainerDescr[P, Array]("Array") implicit def seqDescr[P](implicit childDescr: Description[P]): Description[Seq[P]] = ContainerDescr[P, Seq]("Sequence") implicit val intDescr = AtomDescr[Int]("Integer") implicit val strDescr = AtomDescr[String]("String") println(List(1, 2, 3).describe) println(Array("str1", "str2").describe) println(Seq(Array(List(1, 2), List(3, 4))).describe) 

Displays:


 List of Integer Array of String Sequence of Array of List of Integer 

Description - base type.
ContainerDescr is a recursive class, which, in turn, requires the existence of an implicit Description parameter for the type of container being described.
AtomDescr is a terminal class that describes simple types.


image
Scheme for resolution of implicit parameters.


Debag implicit parameters


When developing with the use of chains of implicit parameters, from time to time you will get compile-time errors, with rather vague names, as a rule, these will be: ambiguous implicit values ​​and diverging implicit expansion. To understand what the compiler wants from you, you need to figure out what these messages mean.


Ambiguous implicit values


As a rule, this error means that there are several conflicting implicit values ​​of the appropriate type in the same scope, and the compiler cannot decide which one to give preference (the order in which the compiler goes through the scope in the search for implicit parameters can be found in this answer ).


 implicit val dog = "Dog" implicit val cat = "Cat" def getImplicitString(implicit str: String): String = str println(getImplicitString) 

When we try to compile this code, we get an error:


 Error:(7, 11) ambiguous implicit values: both value dog in object Example_ambigous of type => String and value cat in object Example_ambigous of type => String match expected type String println(getImplicitString) 

These problems are solved quite obviously - it is necessary to leave only one implicit parameter of this type in context so that the compiler can determine it uniquely.


Diverging implicit expansion


This error means infinite recursion when searching for an implicit value.


 implicit def getString(implicit str: String): String = str println(getString) 

Mistake:


 Error:(5, 11) diverging implicit expansion for type String starting with method getString in object Example_diverging println(getString) 

This kind of error is harder to track. Make sure your recursion has a terminal branch. It often helps to try to explicitly substitute the entire chain of parameters and make sure that this code is compiled.


Compiler flag log-implicits


Also try using the -Xlog-implicits compiler flag - with it, scalac will log the implicit parameter resolution steps and the reasons for the failures.


image
Compiler's messages about candidates for implicit parameters.


Annotation @implicitNotFound


You can mark your classes and traits with the @implicitNotFound annotation to make it more humane to the compiler's messages that an implicit value of this type was not found.


 @implicitNotFound("No member of type class NumberLike in scope for ${T}") trait NumberLike[T] { def plus(x: T, y: T): T def divide(x: T, y: Int): T def minus(x: T, y: T): T } 

Order of declaration of implicit parameters


Descriptions of this aspect could not be found on the Internet and had to clarify it experimentally.


The order of declaration of implicit function parameters is of fundamental importance.


This means that we can use some implicit parameters to limit the visibility of others. For example, if in the field of visibility there are two values ​​of suitable types and we need to choose one of them, without resorting to specifying the desired type.


 sealed trait BaseSought class Target extends BaseSought class Alternative extends BaseSought trait Searchable[T <: BaseSought] implicit def search[T <: BaseSought](implicit canSearch: Searchable[T], sought: T): T = sought implicit val target = new Target() implicit val alt = new Alternative() implicit val canSearchTarget = new Searchable[Target] {} search // : Target 

There are two parameters in scope that fit the desired type [T <: BaseSought], but due to the fact that the implicit Searchable [T] parameter is defined only for one of them, we can determine it unambiguously and not get a compilation error.


image
Successful resolution of implicit parameters.


If we defined the implicit parameters in a different order:


 implicit def search[T <: BaseSought](implicit sought: T, canSearch: Searchable[T]): T = sought 

you would get the error:


 Error:(17, 1) ambiguous implicit values: both value target in object Example11 of type => Example11.Target and value alt in object Example11 of type => Example11.Alternative match expected type T search // : Target 

image
Oops ...


Conclusion


In conclusion, I want to immediately answer the question that will inevitably be asked in the comments: "Why do we need such difficulties? On Go, they live without any generics, let alone such black magic."


Yes, maybe not needed. Implications are definitely not needed if you want to make your code harder with their help. After such languages ​​as, for example, Java, programmers think that if there are many tools in the language, then they should use them all. In fact, you should use complex tools only for complex tasks.


If you can beautifully solve the problem without implications, do it; if not, think again.


If you understand that it may take a significant amount of time for your colleagues to master any tool, but you cannot do without it here, limit its scope, make a library with a simple interface, ensure its quality. And then people will grow to her themselves by the time they come to mind in her to change something.


image


')

Source: https://habr.com/ru/post/329600/


All Articles