Created
May 28, 2019 20:34
-
-
Save chrisalbright/6534ce807dd649d5d71609813c276dbc to your computer and use it in GitHub Desktop.
Exploration of Type Classes in Scala
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
/** | |
* I'm trying to learn type classes in Scala. I've got | |
* a decent handle on Scala itself, but the intricacies | |
* of the Type system are still foreign to me. | |
* | |
* I decided to start with what I thought would be a | |
* simple enough problem: Convert a Thing into a String | |
*/ | |
object TypeClass { | |
/** | |
* So I've got my trait that establishes the behavior | |
* I want. Simple enough. | |
*/ | |
trait AsString[-Thing] { | |
def asString(a: Thing): String | |
def apply(a: Thing): String = asString(a) | |
} | |
object AsString { | |
/** | |
* A static method to do the work | |
*/ | |
def asString[Thing](a: Thing)(implicit ev: AsString[Thing]): String = ev(a) | |
/** | |
* And a syntax module to enrich all the Things | |
*/ | |
implicit class Syntax[Thing](val a: Thing) extends AnyVal { | |
def asString(implicit ev: AsString[Thing]): String = ev(a) | |
} | |
/** | |
* So far so good, this makes sense. Now I'm going to | |
* create some instances... | |
*/ | |
object Instances { | |
/** | |
* These make sense to me, but there is a lot of | |
* duplication... | |
*/ | |
implicit val intAsString: AsString[Int] = | |
new AsString[Int] { | |
override def asString(a: Int): String = a.toString | |
} | |
implicit val booleanAsString: AsString[Boolean] = | |
new AsString[Boolean] { | |
override def asString(a: Boolean): String = a.toString | |
} | |
implicit val bigDecimalAsString: AsString[BigDecimal] = | |
new AsString[BigDecimal] { | |
override def asString(a: BigDecimal): String = a.toString | |
} | |
/** | |
* This is where my understanding falters... | |
* | |
* So it seems like this should work for any T, and it would | |
* reduce the number of instances required, but alas - my | |
* expectations were wrong: | |
* | |
* `asString(3L)` gets me a compile error: | |
* | |
* `Error: could not find implicit value for parameter ev: TypeClass.AsString[Long] | |
* asString(3l)` | |
* | |
* What am I missing here? | |
*/ | |
// implicit def asString[T]: AsString[T] = | |
// new AsString[T] { | |
// override def asString(a: T): String = a.toString | |
// } | |
/** | |
* Additionally, I would expect this to work for both Some[T] and None, | |
* (of course where there is an instance of AsString[T]), but my | |
* understanding is again lacking: | |
* | |
* `asString(Option(7))` works beautifully | |
* | |
* - but | |
* | |
* `asString(Some(7))` gets me | |
* `Error: could not find implicit value for parameter ev: TypeClass.AsString[Some[Int]] | |
* asString(Some(7))` | |
* | |
* - and | |
* `asString(None)` gets me | |
* `Error: could not find implicit value for parameter ev: TypeClass.AsString[None.type] | |
* asString(None)` | |
* | |
*/ | |
implicit def optionAsString[T](implicit ev: AsString[T]): AsString[Option[T]] = | |
new AsString[Option[T]] { | |
override def asString(a: Option[T]): String = a match { | |
case Some(value) => ev(value) | |
case None => "" | |
} | |
} | |
implicit val nothingAsString: AsString[None.type] = | |
new AsString[None.type] { | |
override def asString(a: None.type): String = "" | |
} | |
implicit def seqAsString[T](implicit ev: AsString[T]): AsString[Seq[T]] = | |
new AsString[Seq[T]] { | |
override def asString(a: Seq[T]): String = a.map(ev(_)).mkString(", ") | |
} | |
} | |
} | |
} | |
object RunTypeClass extends App { | |
import TypeClass.AsString | |
import AsString.Instances._ | |
import AsString.Syntax | |
println(Some(1).asString) | |
println(None.asString) | |
println(Seq(1, 2, 3).asString) | |
println(List(1, 2, 3).asString) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment