• en
  • ru

Understanding implicit in Scala

During last year I have had several conversations with my friends from Java world about their experience in Scala. Most of them tried to use Scala as better Java of course and after back with reason – Scala is too powerful and there are many ways to do one thing. The main concern was (surprised 😉 ) about implicit. I agree that implicit is an ambiguous feature of Scala and in the wrong hands, it can be a reason for awful application design and a lot of errors. I think everyone in Scala faced with compilation problems related to implicit resolution and the first time it could be like crashing into the wall – what to do? where to look? how to solve this error? It can make you google issue or even read the documentation! Usually, it solved by adding some imports and we forget about the problem. But why we had to add additional imports? What was the real reason for the issue? In this post, I would like to explain more about implicit usage practices in Scala and I hope they will cease to be scary and confusing.

The most common way implicit used are:

  • Implicit parameters
  • Implicit conversions
  • Implicit classes (“Pimp My Library” pattern)
  • Type classes

There are a lot of articles and documentation about this topic but not a lot of examples how to use it in the real world. In this post, I want to show how is possible to create Scala-friendly API for a beautiful Java library Typesafe Lightbend Config. At first, we must respond to the question – what is wrong with native Java API? Let’s take a look at the example from the documentation:

import com.typesafe.config.ConfigFactory

val conf = ConfigFactory.load();
val foo = config.getString("simple-lib.foo")
val bar = config.getInt("simple-lib.bar")

I see here 2 problems:

  1. Error handling. For example, if the method getInt could not return value of proper type it will throw an exception. But we want to write clear code without exceptions.
  2. Extensibility. This API has support for some default Java types but what if we would like to add more? For example, LocalDate or Scala native types.

Let’s start with the second problem. The Java way to solve it – inheritance. We can extend a base class with new functionality and add required methods. It can work if you are the owner of the code otherwise usually it’s problematic. Here we can remember we write on Scala and try to use “Pimp My Library” pattern:

implicit class RichConfig(val config: Config) extends AnyVal {
  def getLocalDate(path: String): LocalDate = LocalDate.parse(config.getString(path), DateTimeFormatter.ISO_DATE)
}

And after we can use as it was defined in original code:

val date = config.getLocalDate("some-string-date")

Not bad. But we still need to keep all extensions in one RichConfig or have potential “Ambiguous implicit values” error if the same functionality will define in separate implicit classes. “Pimp My Library” is great and power pattern but must be used neatly and in place.

Can we do better? Initially let’s remember that inheritance in Java usually used as a way to implement polymorphism. It’s so-called subtype polymorphism. But there are other types of polymorphism. Wikipedia says at least about:

  1. Subtyping
  2. Parametric polymorphism
  3. Ad-hoc polymorphism

One interesting for us is the ad-hoc polymorphism.

In programming languages, ad-hoc polymorphism is a kind of polymorphism in which polymorphic functions can be applied to arguments of different types, because a polymorphic function can denote a number of distinct and potentially heterogeneous implementations depending on the type of argument(s) to which it is applied. It is also known as function overloading or operator overloading. The term ad hoc in this context is not intended to be pejorative; it refers simply to the fact that this type of polymorphism is not a fundamental feature of the type system.

Wikipedia

In Scala ad-hoc polymorphism can be implemented with type class pattern. It came from Haskel where type classes are part of the language. Simply it’s a generic Foo[T] parametrised by some type T and this type used in implicit resolution to get a required implementation of Foo[T]. Maybe sounds complex but it’s simple and fun.

For example, we can define generic config reader as:

trait Reader[A] {
  def read(config: Config, path: String): Either[Throwable, A]
}

Either here to solve the first problem – error handling. No more exceptions! We can define a type alias for our result type and add few implementations.

trait Reader[A] {
  def read(config: Config, path: String): Reader.Result[A]
}

object Reader {
  type Result[A] = Either[Throwable, A]

  def apply[A](read: (Config, String) => A): Reader[A] = new Reader[A] {
    def read[A](config: Config, path: String): Result[A] = Try(read(config, path)).toEither
  }

  implicit val intReader = Reader[Int]((config: Config, path: String) => config.getInt(path))
  implicit val stringReader = Reader[String]((config: Config, path: String) => config.getString(path))
  implicit val localDateReader = Reader[LocalDate]((config: Config, path: String) => LocalDate.parse(config.getString(path), DateTimeFormatter.ISO_DATE);)
}

So we have created type class Reader with implementations for Int, String and LocalDate types. Now we need to make config to work with our Reader. And here is a good way to use implicit classes to extend the syntax of original Config class and implicit parameter to pass Reader instance of a required type:

implicit class ConfigSyntax(config: Config) extends AnyVal {
  def as[A](path: String)(implicit reader: Reader[A]): Reader.Result[A] = reader.read(config, path)
}

We can even rewrite it to cleaner form using context bounds:

implicit class ConfigSyntax(config: Config) extends AnyVal {
  def as[A : Reader](path: String): Reader.Result[A] = implicitly[Reader[A]].read(config, path)
}

And usage example:

val foo = config.as[String]("simple-lib.foo")
val bar = config.as[Int]("simple-lib.bar")

Type classes are a power tool to extend functionality without changing existing code. It gives much more freedom and flexibility comparing with subtyping. If you need add support for new type – just implement required type class instance and put in the scope. Also, it’s simple to override default implementation using implicit resolution order. For example, we can define our custom LocalDate reader and it will be used instead of default one:

implicit val localDateReader2 = Reader[LocalDate]((config: Config, path: String) =>
  Instant
    .ofEpochMilli(config.getLong(path))
    .atZone(ZoneId.systemDefault())
    .toLocalDate()
)

As we can see implicit when used properly can help a lot to write clean and extensible code. Implicit can help you extend third-party libraries without code modification and write cleaner code with implicit parameters. They are fine if you write generic code and want to use ad-hoc polymorphism with type classes. You should not worry about complex class hierarchy just split your code into functional parts and implement it separately. «Divide and rule» in action.

Github link to scala-config project.

  •  
  •  
  •  
  •  
  •  
  •  

This post is also available in: Russian

Всё самое свежее и интересное я теперь публикую в своём Телеграм-канале.


No Comments :

Leave a Reply :

* Your email address will not be published.

Вход через соцсети