Last active
January 5, 2023 12:03
-
-
Save frgomes/eb8efc3224fb102a079dc2fb34ccf630 to your computer and use it in GitHub Desktop.
Scala - typeclasses - simple tutorial
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
// QUICK EXAMPLE FOR THE IMPATIENT | |
/** This trait provides ability to instantiate a generic class */ | |
trait Makeable[T] { | |
def make: T | |
} | |
/** This typeclass which allows making an instance of type `T` */ | |
class Make[T: Makeable] { | |
def make(): T = implicitly[Makeable[T]].make | |
} | |
// implicit helper objects for typeclass `Make` | |
implicit object MakeableString extends Makeable[String] { def make: String = "" } | |
implicit object MakeableString extends Makeable[Int] { def make: Int = 0 } | |
implicit object MakeableString extends Makeable[Long] { def make: Long = 0L } | |
implicit object MakeableString extends Makeable[Float] { def make: Float = 0.0f } | |
implicit object MakeableString extends Makeable[Double] { def make: Double = 0.0 } | |
// TUTORIAL WITH EXPLANATIONS FOR EACH STEP | |
/* | |
* The first step when defining a typeclass consists on | |
* defining the trait it implements. | |
*/ | |
trait Size[A] { | |
def size(a: A): Int | |
} | |
/* | |
* The second step when defining a typeclass consists on | |
* defining a companion object for the typeclass trait giving some useful default implementations. | |
* | |
* These useful default implementations are provided as implicit objects and/or implicit functions, which | |
* can be found after local implicits so that you can still override the default implementations if you wish. | |
* | |
* See more about implicit search order here: | |
* https://stackoverflow.com/questions/5598085/where-does-scala-look-for-implicits/5598107#5598107 | |
*/ | |
object Size { | |
implicit object IntSize extends Size[Int] { def size(a: Int) = a } | |
implicit object StringSize extends Size[String] { def size(a: String) = a.length } | |
/* This one is more elaborated: SizeLike[List[A]] needs SizeLike[A], which can be found implicitly. */ | |
implicit def ListSize[A](implicit itemSized: Size[A]) = new Size[List[A]] { | |
def size(a: List[A]): Int = a.map(item => itemSized.size(item)).sum | |
} | |
} | |
/* | |
* The third step (and the last step!) when defining a typeclass consists on | |
* defining a convenience trait, useful on the calling site, which requires implicit argument(s). | |
* | |
* This step is not a requirement but usually provided as a sake of convenience. | |
* | |
* Notice how this trait is similar to the typeclass trait. | |
* It usually only wires an implicit argument and employs it in order to arrive to a concrete implementation. | |
*/ | |
trait SizeImpl { | |
def size[A](a: A)(implicit o: Size[A]): Int = o.size(a) | |
} | |
//----- | |
// This is a simple example which uses the typeclass | |
object Example1 extends SizeImpl { | |
def main = { | |
println(size(2)) // prints 2 | |
println(size("abc")) // prints 3 | |
println(size(List("abc", "d", "efgh"))) // prints 8 | |
} | |
} | |
Example1.main | |
//----- | |
// This example supplies an implicit implementation | |
object Example2 extends SizeImpl { | |
implicit object BooleanSize extends Size[Boolean] { def size(a: Boolean) = if(a) 1 else 0 } | |
def main = { | |
println(size(false)) // prints 0 | |
println(size(true)) // prints 1 | |
} | |
} | |
Example2.main | |
//----- | |
// This example supplies an implicit implementation which is used by a default implementation | |
object Example3 extends SizeImpl { | |
implicit object BooleanSize extends Size[Boolean] { def size(a: Boolean) = if(a) 1 else 0 } | |
def main = { | |
println(size(List(true, false, true, false, true))) // prints 3 | |
} | |
} | |
Example3.main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment