Friday, April 17, 2020

Implicits in Scala for dummies

The purpose of this post is to demystify implicits in Scala, at least a little bit :)

1. Implicit parameters. Context bounds.

1.1. Implicit parameters

Implicit parameters - most likely the simplest form of implicits. Let's see how they works:

Let's say we have some function:

private def printValue[T](value: T): Unit = {
  println(value)
}

which we can call this way:

printValue(123)
printValue("first")

And, at some point we decided that it's better to have some modification of data which we want to print, something like:

printValue(transform(123))
printValue(transform("first"))

But what is if we have a lot of such calls ? It may take some time to modify them all! Or may be we just too lazy  :) As a solution we can use implicit parameter here:


trait Transform[T] {
  def transform(value: T): T}

implicit def intTransform: Transform[Int] = _ * 10 + 1
implicit def stringTransform: Transform[String] = _.toUpperCase
private def printValue[T](value: T)(implicit transformer: Transform[T]): Unit = {
  println(transformer.transform(value))
}

// explicit call
printValue(234)(intTransform)
printValue("first")(stringTransform)

// implicit call
printValue(567) printValue("second")

First, we defined 2 transformers for Int and Sring type and marked them as implicit. Second, we added parameter to our "printValue" function and marked it as implicit as well. Now, if we want - we can pass this transformers as a parameter explicitly. But if we don't - they will be passed implicitly - automatically.

Thus, we don't have to change code which is calling this function, but may change the behavior of function.

Execution results:
2341
FIRST

5671
SECOND

1.2 Context bound

Also starting from Scala 2.8 we can simplify this implicit function a little bit:

// Scala 2.8 allows a shorthand syntax for implicit parameters, called Context Bounds.
private def printValue2[T: Transform](value: T): Unit = {
  val transformer = implicitly[Transform[T]]
  println(transformer.transform(value))
}

printValue2(678)
printValue2("third")


Execution results:
6781
THIRD

This is called "context bounds"



2. Implicit type conversion. Pimp my library. View bounds.

2.1. Implicit type conversion


Also implicits can be useful for different type conversions. 

Let's say we have some class and some function which is doing something very important with parameter of this class:

case class StringContainer(value: String) {
  def sayHi(): Unit = println("Hello world!")
}

def printStringContainer(value: StringContainer): Unit = println(s"=== ${value.value.toUpperCase} ===")

Later we created one more class:

case class IntContainer(value: Int)



  and now we're thinking that it would be great to use "printStringContainer" function for "IntContainer" class as well. But it's not possible because parameter for this function is of type "StringContainer". So we should create a converter from IntContainer to StringContainer.

Let's do it and let's mark it "implicit":

implicit def intToString(intContainer: IntContainer): StringContainer = intContainer match {
  case IntContainer(1) => StringContainer("One")
  case IntContainer(2) => StringContainer("Two")
  case _ => StringContainer("I don't know")
}


And now conversion from IntContainer to StringContainer will happen implicitly - we can just use IntContainer as StringContainer:

printStringContainer(IntContainer(1))

val intVal: IntContainer = IntContainer(2)
val stringVal: StringContainer = intVal


2.2. Pimp my library pattern

If you remember from example from above class StringContainer has method "sayHi()":

case class StringContainer(value: String) {
  def sayHi(): Unit = println("Hello world!")
}


Int container  - don't have it:
case class IntContainer(value: Int)

But we have implicit converter from IntContainer to StringContainer:
implicit def intToString(intContainer: IntContainer): StringContainer = intContainer match {
  case IntContainer(1) => StringContainer("One")
  case IntContainer(2) => StringContainer("Two")
  case _ => StringContainer("I don't know")
}

And now we can do really interesting stuff with IntContainer: we can call from variable of this type method which it don't have:

// Pimp My Library//  - we're calling method from StringContainer on IntContainer variable
//     intContainer type will be converted into stringContainerTypeintVal.sayHi()


This is called ''pimp my library" pattern. We "extended" one class by functionality from another class by just implicit conversion function.

2.3 Conversion chains

We can have several conversion like A=>B=>C let's see an example:

case class IntContainer(value: Int)

case class StringContainer(value: String)

case class BooleanContainer(value: Boolean) {
  def yesOrNo(): Unit = if (value) println("Yes") else println("No")
}

implicit def intToString(value: IntContainer): StringContainer = value match {
  case IntContainer(1) => StringContainer("One")
  case IntContainer(2) => StringContainer("Two")
  case _ => StringContainer("I don't know")
}

implicit def stringToBooleanType[T](value: T)(implicit toString: T => StringContainer): BooleanContainer = {
  val stringValue = toString(value)
  BooleanContainer(stringValue != null && stringValue.value != null && stringValue.value != "I don't know")
}


val intVal2: IntContainer = IntContainer(2)
val intVal3: IntContainer = IntContainer(3)

// chain conversion intType to stringType to booleanType
val booleanVal2: BooleanContainer = intVal2val booleanVal3: BooleanContainer = intVal3
// pimp my libraryintVal2.yesOrNo()

2.4 View bounds(type classes)

Sometimes we know that some classes can be converted to another (as in examples from above). This can be also called "view bounds": class can be "viewed"(converted) as (to) another. Let's create an example of class which works for all classes which can be viewed as "StringContainer" class. Such view bounds we can define using: [T <% StringContainer] syntax.

// view boundclass StringContainerUtils[T <% StringContainer] {
  def valueLength(stringContainer: T) = stringContainer.value.length}

val  stringContainerUtils = new StringContainerUtils()
stringContainerUtils.valueLength(intVal)

As you can see, we can pass intVal of IntContainer type as a parameter into function which expect StringContainer type.


3. Type conversion using implicit classes

Actually, implicit classes correlates with implicit conversions from previous chapter.
Such classes should have just one parameter and all variables of this parameter type will have implicitly methods of such class. So, it's just another form of "pimp my library" pattern:


// it should have constructor with exact one parameterimplicit class IntToString(value: Int) {
  def intToString(): String = value match {
    case 1 => "One"    case 2 => "Two"    case _ => "I don't know"  }
}

println(1.intToString())
println(2.intToString())
println(3.intToString())

- we defined such class for Int type, so now all variables/values of Int type can use methods of this class.

We can of course have more complicated constructions like implicit classes with implicit methods:

sealed trait MyContainercase class IntContainer(value: Int) extends MyContainer
case class StringContainer(value: String) extends MyContainer
trait Info[T <: MyContainer] {
  def showInfo(): String}

implicit val intTypeInfo: Info[IntContainer] = () => "This is IntType"
implicit val stringTypeInfo: Info[StringContainer] = () => "This is StringType"
// we still should have constructor parameter even if we don't use it explicitly
implicit class MyTypeTools[T <: MyContainer](value: T) {
  def getInfo(implicit info: Info[T]): String = info.showInfo()
}

val intVal = IntContainer(1)
println(intVal.getInfo)

val stringVal = StringContainer("one")
println(stringVal.getInfo)

4. The end

As you can implicits are really great and can make coding much more exciting :)  

No comments:

Post a Comment