Thursday, December 21, 2017

Common forms of Type class pattern in Scala

This post will explore some of the various forms the type class encoding in Scala can take.

It is the 5th post in a series of posts on type class encoding in Scala. Check the introductory post: Exploring Typeclass in Scala: A knowledge pack, if you want to start reading the series from the beginning.

Typeclass is not a native language feature but a pattern in Scala, which might make it hard to easily spot when used in a codebase. To add to this confusion, there could also be slight variations to the approach taken to encode the pattern in Scala.


You can see this as the existence of different patterns when encoding the typeclass pattern.

In Encoding Type class in Scala, I showed the essential components of the type class pattern. Which are, the type class, the instance of the type class and the mechanism that allows the application of a polymorphic operation to an object of a particular type using its type class instances.

One guiding thought that would help when exploring the various forms the type class pattern could take is to always be aware that the goal is to have a mechanism where you can have an object, and its type class instance available together for use.

Note that there are ways to orchestrate this making use of plugins/libraries that help in reducing the verbosity involved in encoding the type class pattern in Scala. Libraries like Dandy or Simulacrum. This post would not touch on how to use these libraries. The various forms mentioned in this post are all achieved using vanilla Scala.

With this at the back of our mind, let's go ahead and explore the various forms of the type class encoding you might run into.
  1. Interface Objects
    • Basic form
    • Context bounds
    • Direct Access via Apply method and implicit parameter
    1. Type Enrichments

    Interface Objects

    The Interface Objects approach consists of the various approaches that involve having an object expose methods through which a value of a particular type, together with their type class instances can be provided. I outline the various forms of it, that I have seen in the wild:

    Basic form
    The most basic form of the mechanism that allows for the application of the polymorphic operation using typeclass instances is to have a method that takes the data to be operated and have the typeclass instance for that data marked as an implicit parameter. For example in the following code:

    // type class
    trait Reversible[A] {
      def reverse(data:A): A
    }
    
    // type class instance for Int
    implicit object intReversible extends Reversible[Int] {
      override def reverse(data: Int) = data.toString.reverse.toInt
    }
    
    // type class instance for String
    implicit object stringReversible extends Reversible[String] {
      override def reverse(data: String) = data.reverse
    }
    
    // basic object interface
    object reverser {
      def reverse[T](data: T)(implicit reversible: Reversible[T]) = {
        reversible.reverse(data)
      }
    }
    
    // usage of the object interface
    reverser.reverse("Ajala")
    reverser.reverse(12345)
    

    The object interface is

    object reverser {
      def reverse[T](data: T)(implicit reversible: Reversible[T]) = {
        reversible.reverse(data)
      }
    }
    

    and when it is used, for example in:

    reverser.reverse(12345)
    

    The type that is being operated on is Int (since 12345 is Int) and the type class instance is intReversible which would be passed to the reverse method via implicit resolution.

    With Context Bound
    We can modify the basic version if we want by using the context bound annotation. If you do not have a working understanding of what context bounds annotation is, you can see it as a simple mechanism to tell the computer to treat type annotations like [A: Context] to mean there is a type A and an implicit value of Context[A] in the implicit scope. For more information on Context bound, check Exploring Type Annotations in Scala

    If we use the context bound annotation, the code will look like:

    // type class
    trait Reversible[A] {
      def reverse(data:A): A
    }
    
    // type class instance for Int
    implicit object intReversible extends Reversible[Int] {
      override def reverse(data: Int) = data.toString.reverse.toInt
    }
    
    // type class instance for String
    implicit object stringReversible extends Reversible[String] {
      override def reverse(data: String) = data.reverse
    }
    
    // basic object interface
    object reverser {
      def reverse[T: Reversible](data: T) = {
        implicitly[Reversible[T]].reverse(data)
      }
    }
    
    // usage of the object interface
    reverser.reverse("Ajala") // prints alajA
    reverser.reverse(12345) // prints 54321
    

    Notice that we also introduce the implicitly function. Which allows the taking hold of the implicit value within the implicit scope. This might not be necessary in cases where we do not need direct access to the type class instance. In such cases, we just need to bring the implicit value into the local scope of the method and having the context bound would suffice.

    Direct Access via Apply method and implicit parameter
    I sometimes call this fishing with implicit and types :)

    In some situations, you just need to grab a hold of the type class instance for a particular type and directly call a single method on the type class instance. In such a case, using any of the previously described forms feels like some indirection. Since you need to expose a method with implicit parameters in other to get a hold of a type class instance, so as to be able to call one method on the typeclass instance within the method you are exposing.

    Why not have a way to directly fish the typeclass instance you need and call the method on it directly?

    This is what this form allows. It makes use of the fact that in Scala if you name an empty-paren method "apply", then you can omit the writing of the "apply" when you call the "apply" method. For example:

    object Yo {
      def apply(): Unit = println("Yo Hommie")
    }
    
    // Same as Yo.apply()
    Yo()
    

    So applying this feature of Scala, together, again with implicit parameters, you have an ability to directly fish out a type class instance based on type. This would involve setting up the object interface in a slightly different form

    // type class
    trait Reversible[A] {
      def reverse(data:A): A
    }
    
    // type class instance for Int
    implicit object intReversible extends Reversible[Int] {
      override def reverse(data: Int) = data.toString.reverse.toInt
    }
    
    // type class instance for String
    implicit object stringReversible extends Reversible[String] {
      override def reverse(data: String) = data.reverse
    }
    
    // basic object interface
    object reverser {
      def apply[T](implicit reversible: Reversible[T]) = {
        reversible
      }
      def reverse[T](data: T)(implicit reversible: Reversible[T]) = {
        reversible.reverse(data)
      }
    }
    
    // directly access the type class instances
    reverser[String].reverse("Ajala") 
    reverser[Int].reverse(12345)
    

    In this case, this mechanism allows us to achieve the aim of the type class pattern orchestration. It allows for the selection of the type class instance, and also the provision of the object of the type class instance to be worked on: which is what we provide when we call the method on the instance.

    Type Enrichment

    Type enrichment is as a result of the application of implicit classes. It is another application of the implicit mechanism in Scala.

    In essence, it involves converting a type, when a method that does not exist on it is called, into another type that has that method, using a defined implicit class in scope.

    For example:

    implicit class EnrichedInt(val value: Int) {
      def isEven = {
        value % 2 == 0
      }
    }
    
    2.isEven // true
    3.isEven // false
    

    Where the presence of the implicit class EnrichedInt, allows for the transparent conversion of Int to EnrichedInt when a method isEven which is not defined on Int is called on values of type Int.

    This process of type enrichment is one that I have also seen applied when encoding the type class pattern. It basically involves enriching the type with a method that accepts the typeclass instance.
    // type class
    trait Reversible[A] {
      def reverse(data:A): A
    }
    
    // type class instance for Int
    implicit object intReversible extends Reversible[Int] {
      override def reverse(data: Int) = data.toString.reverse.toInt
    }
    
    // type class instance for String
    implicit object stringReversible extends Reversible[String] {
      override def reverse(data: String) = data.reverse
    }
    
    implicit class FlipOps[T](value: T) {
      def flip(implicit reversible: Reversible[T]) = {
        reversible.reverse(value)
      }
    }
    
    // usage of the enriched type
    12345.flip // 54321
    "Ajala".flip // alajA
    

    By convention, the implicit class defined in this situation are always suffixed with Ops, hence the FlipOps in the example above.

    The type enrichment pattern also achieves the aim of providing a mechanism for getting access to both a type and its type class instance. In the example above, the type is the type of value passed into the constructor of the implicit class, while the type class instance is passed via the method the implicit class exposes.

    Conclusion

    There could be various ways to assemble the various components of the type class pattern together. To a large extent though, the ways are a minor modification of the same general idea. Which is to make it possible to access a typeclass instance together with the object of the type to be worked on.

    Next post, Three Real-world examples of Type class pattern in Scala, I take a look at a couple of examples of the type class patterns.

    No comments: