A typeclass example taken from the Programming Scala book:
case class Address(street: String, city: String)
case class Person(name: String, address: Address)
trait ToJSON {
def toJSON(level: Int = 0): String
val INDENTATION = " "
def indentation(level: Int = 0): (String,String) =
(INDENTATION * level, INDENTATION * (level+1))
}
implicit class AddressToJSON(address: Address) extends ToJSON {
def toJSON(level: Int = 0): String = {
val (outdent, indent) = indentation(level)
s"""{
|${indent}"street": "${address.street}",
|${indent}"city": "${address.city}"
|$outdent}""".stripMargin
}
}
implicit class PersonToJSON(person: Person) extends ToJSON {
def toJSON(level: Int = 0): String = {
val (outdent, indent) = indentation(level)
s"""{
|${indent}"name": "${person.name}",
|${indent}"address": ${person.address.toJSON(level + 1)}
|$outdent}""".stripMargin
}
}
val a = Address("1 Scala Lane", "Anytown")
val p = Person("Buck Trends", a)
println(a.toJSON())
println()
println(p.toJSON())
The code works fine, but I am under the impression (from some blog posts) that typeclasses are typically done this way in scala:
// src/main/scala/progscala2/implicits/toJSON-type-class.sc
case class Address(street: String, city: String)
case class Person(name: String, address: Address)
trait ToJSON[A] {
def toJSON(a: A, level: Int = 0): String
val INDENTATION = " "
def indentation(level: Int = 0): (String,String) =
(INDENTATION * level, INDENTATION * (level+1))
}
object ToJSON {
implicit def addressToJson: ToJSON[Address] = new ToJSON[Address] {
override def toJSON(address: Address, level: Int = 0) : String = {
val (outdent, indent) = indentation(level)
s"""{
|${indent}"street": "${address.street}",
|${indent}"city": "${address.city}"
|$outdent}""".stripMargin
}
}
implicit def personToJson: ToJSON[Person] = new ToJSON[Person] {
override def toJSON(a: Person, level: Int): String = {
val (outdent, indent) = indentation(level)
s"""{
|${indent}"name": "${a.name}",
|${indent}"address": ${implicitly[ToJSON[Address]].toJSON(a.address, level + 1)}
|$outdent}""".stripMargin
}
}
def toJSON[A](a: A, level: Int = 0)(implicit ev: ToJSON[A]) = {
ev.toJSON(a, level)
}
}
val a = Address("1 Scala Lane", "Anytown")
val p = Person("Buck Trends", a)
import ToJSON.toJSON
println(toJSON(a))
println(toJSON(p))
Which way is better or more correct? Any insights are welcome.
It's a stretch to call the first ToJSON
a "type class" at all (although it's not like these terms are standardized, and even your second, more Scala-idiomatic version differs in many important ways from e.g. type classes in Haskell).
One of the properties of type classes that I would consider definitional is that they allow you to constrain generic types. Scala provides special syntax to support this in the form of context bounds, so I can write e.g. the following:
import io.circe.Encoder
def foo[A: Numeric: Encoder](a: A) = ...
This constrains the type A
to have both Numeric
and Encoder
instances.
This syntax isn't available for the first ToJSON
, and you'd have to use something like view bounds (now deprecated) or implicit implicit conversion parameters instead.
There are also many kinds of operations that can't be provided by the first ToJSON
style. For example, suppose we've got a Monoid
that uses the standard Scala encoding of type classes:
trait Monoid[A] {
def empty: A
def plus(x: A, y: A): A
}
And we wanted to translate it into the first style, where we have an unparametrized Monoid
trait that will be the target of implicit conversions from types that we want to be able to treat as monoidal. We're entirely out of luck, since we don't have a type parameter that we can refer to our empty
and plus
signatures.
One other argument: the type classes in the standard library (Ordering
, CanBuildFrom
, etc.) all use the second style, as do the vast majority of third-party Scala libraries you'll come across.
In short, don't ever use the first version. It'll only work when you only have operations of the form A => Whatever
(for some concrete Whatever
), doesn't have nice syntactic support, and isn't generally considered idiomatic by the community.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With