Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different ways of building typeclasses in Scala?

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.

like image 350
qed Avatar asked Feb 29 '16 16:02

qed


1 Answers

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.

like image 86
Travis Brown Avatar answered Oct 01 '22 15:10

Travis Brown