The power of Scala Type Classes
There are always several ways and styles of thinking to address problems. Type classes is another way of thinking and dealing with problems. For some problems, you might just use Pattern Matcher -- But if you have to add a type? Right, I could stick with good old OO since Scala is hybrid right? -- But what if you need to add one operator? Type Classes can help you out.
What Are Type Classes?
In order to have a type class you need to have:
1. A signature
2. All implementations for Supported Types(You can add more types later)
3. A function that requires a Type Classe
What are the Use Cases for Type Classes?
Type classes are great because of the make behavior more explicit and extendable. This "Behavior" more explicit happens because this will be checked at Compile Time. So you can extend and add more Behavior without re-compiling or be changing your previous code. That's why type classes are powerful.
What are the practical Use cases?
1. Business Rules / Domain Objects: Where you need to express different and customizable calculations. Doing this changes without breaking the client.
2. Pipelining: When you need to apply several different rules or a request.
3. Serialization/Deserialization: When you need express different parsing logic.
Sample Code
Now let's see some sample code. First code we will define some simple Math operations and we will use Implicits to apply automatic conversion for use.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.github.diegopacheco.scalaplayground.typeclasses | |
object NumbersMainApp extends App { | |
trait NumberLike[T] { | |
def plus(x: T, y: T): T | |
def divide(x: T, y: T): T | |
def minus(x: T, y: T): T | |
def multiply(x: T, y: T): T | |
} | |
object NumberLike { | |
implicit object NumberLikeDouble extends NumberLike[Double] { | |
def plus(x: Double, y: Double): Double = x + y | |
def divide(x: Double, y: Double): Double = x / y | |
def minus(x: Double, y: Double): Double = x - y | |
def multiply(x: Double, y: Double): Double = x * y | |
} | |
implicit object NumberLikeInt extends NumberLike[Int] { | |
def plus(x: Int, y: Int): Int = x + y | |
def divide(x: Int, y: Int): Int = x / y | |
def minus(x: Int, y: Int): Int = x - y | |
def multiply(x: Int, y: Int): Int = x * y | |
} | |
implicit def int2NumberLikeInt(x: Int) = NumberLikeInt | |
implicit def double2NumberLikeDouble(y: Double) = NumberLikeDouble | |
} | |
import NumberLike._ | |
val x = 10 | |
println(s"10 + 10 = ${x.plus(10, 10)}") | |
println(s"10 - 10 = ${x.minus(10, 10)}") | |
println(s"10 * 10 = ${x.multiply(10, 10)}") | |
println(s"10 / 10 = ${x.divide(10, 10)}") | |
val y:Double = 20.5 | |
println(s"20.5 + 20.5 = ${y.plus(20.5, 20.5)}") | |
println(s"20.5 - 20.5 = ${y.minus(20.5, 20.5)}") | |
println(s"20.5 * 20.5 = ${y.multiply(20.5, 20.5)}") | |
println(s"20.5 / 20.5 = ${y.divide(20.5, 20.5)}") | |
} |
So here we have a Trait called NumberLike witch is expecting a Class called T. Which could be any numeric type. Them we can a companion object with added support for 2 types Int and Double. This support is added via implicit objects called NumberLikeInt and NumberLikeDouble.
Finally, on the same object, we have 2 implicit conversions one to convert an Int into a NumberLikeInt and another to convert Doubles in NumberLikeDouble. Once we did the import on the main method we can use regular Int and Double add call our talk math methods.
The second sample is about behavior with different objects and the ability to add things later. So let's take a look at the following code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.github.diegopacheco.scalaplayground.typeclasses | |
object CanTalkMainApp extends App { | |
final case class Person(name:String) | |
final case class Cat(name:String) | |
trait CanTalk[T]{ | |
def talk(talker:T): String | |
} | |
object AddOns{ | |
implicit object PersonTalker extends CanTalk[Person]{ | |
def talk(p:Person):String = s"Hi, I'm ${p.name}" | |
} | |
implicit object CatTalker extends CanTalk[Cat]{ | |
def talk(c:Cat):String = s"burun burun burun (${c.name})" | |
} | |
implicit class TalkUtil[A](x:A){ | |
def talk(implicit talker:CanTalk[A]):String = talker.talk(x) | |
} | |
} | |
import AddOns._ | |
println( Person("Diego").talk ) | |
println( Cat("Gandalhy").talk ) | |
final case class Dog(name:String) | |
implicit object DogTalker extends CanTalk[Dog]{ | |
def talk(d:Dog):String = s"rof rof (${d.name})" | |
} | |
import DogTalker._ | |
println( Dog("Dunginha").talk ) | |
} |
Here we have 2 case classes: Person and Cat. We also have a Trait Can talk which expecting a Type T where T can talk. On AddOns companion object we have some default behavior implementations like PersonTalker and CatTalker witch allow Person and Cat to talk.
We also have an implicit class wrapper which gets a Type A and parameter X. This class provides the talk method where the CanTalk[A] will be implicit and will delegate to method talk passing the X argument.
Right after doing the write import we can call talk method on case class Person and Cat. We also can add new types so I defined a Dog after the fact.
Scala Type classes are really powerful and cool it allows to do really modular design and extend the design in a very clean and concise way. You can download the full source code and SBT project on my github here.
Cheers,
Diego Pacheco