Scala

Scala typeclass explained: Implement a String.read function

In this short article I’d like to explain how you can use the typeclass pattern in Scala to implement adhoc polymorphism. We won’t go into too much theoretical details in this article, the main thing I want to show you is how easy is really is to create a flexible extensible model without tying your domain model to specific implementation or traits. There are many resources regarding scala typeclasses available on the internet, but I couldn’t find a good reference, so that’s why I created this one. In this example we’ll look at a very pragmatic (and naive) implementation of the read typeclass from haskell. This type class allows you to convert a string to a specific type in a generic way.

What we want to accomplish with this typeclass is the following (The complete example can be found here: https://gist.github.com/josdirksen/9051baf09003dac37386)

  println(Readable[Double].read("10"))
  println(Readable[Int].read("10"))
  println(Readable[String].read("Well duh!"))
  println(Readable[List[Char]].read("Well duh!"))
  println(Readable[List[String]].read("Using:A:Separator:to:split:a:String"))

  // we can also use the read function directly
  println("20".read[Double]);
  println("Using:A:Separator:to:split:a:String".read[List[Char]]);
  println("Using:A:Separator:to:split:a:String".read[List[String]]);
  println(Readable[Task].read("10|Title Text|Title Content"))
  println("20|Another title Text|Another title Content".read[Task])

With the Readable typeclass we provide a generic way to convert a String to a specific type. In the example above we used the type class to convert a String to some basic types, but also to different lists and a specific case class. The functionality for the basic types isn’t really that useful, since the scala String object already provides the toDouble, toString, etc. functions. This way, however, you don’t need to know the specific function to call, but you can just specify the target type you want :) It, however, gets much more interesting with the Task case class you see here. As you’ll see in the rest of the code, through the use of implicits we can simply add support for this case class, without having to change the implementation of either the String class or the Task class.

To implement a typeclass we first have to define the trait that defines the functions we need to implement. In this sample, we have only one function:

  /**
   * The readable trait defines how objects can be converted from a string
   * representation to the objects instance. For most of the standard types
   * we can simply use the toType function of String.
   */
  trait Readable[T] {
    def read(x: String): T
  }

The read function should just transform the String to the specified type T. Now that we’ve defined our trait, lets look at the companion object, which contains some helper classes and simple implementations:

  /**
   * Companion object containing helper functions and standard implementations
   */
  object Readable {

    /**
     * Helper function which allows creation of Readable instances
     */
    def toReadable[T](p: String => T): Readable[T] = new Readable[T] {
      def read(x: String): T = p(x)
    }

    /**
     * Allow for construction of standalone readables, if the ops aren't used
     */
    def apply[A](implicit instance: Readable[A]): Readable[A] = instance

    // Using the toReadable creates cleaner code, we could also explicitly
    // define the implicit instances:
    //
    //   implicit object ReadableDouble extends Readable[Double] {
    //      def read(s: String): Double = s.toDouble
    //    }
    //    implicit object ReadableInt extends Readable[Int] {
    //      def read(s: String): Int = s.toInt
    //    }
    implicit val ReadableDouble = toReadable[Double](_.toDouble)
    implicit val ReadableInt = toReadable[Int](_.toInt)
    implicit val ReadableLong = toReadable[Long](_.toLong)
    implicit val ReadableString = toReadable[String](new String(_))
    implicit val ReadableBoolean = toReadable[Boolean](_.toBoolean)
    implicit val ReadableCharList = toReadable[List[Char]](_.toCharArray.toList)
    implicit val ReadableStringList = toReadable[List[String]](_.split(':').toList)

  }

The code shouldn’t be that hard to understand. What we do here, is we create a number of Readable implementations. It’s important to note that we use the implicit parameter, so that we can pull them into scope later on. At this point we can already start using the typeclass. For that we import the implicits and use the Readable type class like this:

  import Readable._
  import Readable.ops._

  // now we can just get an instance of a readable and call the read function
  // to parse a string to a specific type.
  println(Readable[Double].read("10"))
  println(Readable[Int].read("10"))
  println(Readable[String].read("Well duh!"))
  println(Readable[List[Char]].read("Well duh!"))
  println(Readable[List[String]].read("Using:A:Separator:to:split:a:String"))

Easy right? Scala will look for an implicit that matches the specified type and convert the String to that type. This is nice, but doesn’t really look that nice. We need to instantiate a Readable (even though that isn’t too much code) and call read to convert the String. We can make it easier by extending the String class with a read function that does the conversion for us. For this add the following to the Readable companion object.

    /**
     * Extend the string object with a read function.
     */
    object ops {
      implicit class pp[T](s: String) {

        /**
         * The type parameter should have an implcit Readable in scope. Use
         * implicitly to access it and call the read function
         */
        def read[T: Readable]= implicitly[Readable[T]].read(s)
      }
    }

These operation define an implicit transformation which add the read function the String object. All that is left is to import the ops implicit functions and we can use the read directly:

  import Readable.ops._
  // we can also use the read function directly
  println("20".read[Double]);
  println("Using:A:Separator:to:split:a:String".read[List[Char]]);
  println("Using:A:Separator:to:split:a:String".read[List[String]]);

This same approach can also be used to create a read function for case classes:

  // creating custom read function can be done without tying a case class
  // to the reads implementation. In the following sample assume we
  // serialize it to a string with | as separators:
  //     10|Title Text|Title Content
  case class Task(id: Long, title: String, content: String)

  // simple convert the incoming string to a Task
  implicit val readableTask = toReadable(
    _.split('|') match {
      case Array(id: String, title: String, content: String) => new Task(id.read[Long], title, content)
    }
  )

  println(Readable[Task].read("10|Title Text|Title Content"))
  println("20|Another title Text|Another title Content".read[Task])

And that’s pretty much it. By just implementing the Readable[Task] trait our custom Task case class can be processed in the same manner as the other objects.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button