St.Antario
St.Antario

Reputation: 27385

TypeClasses in ScalaZ

I'm reading ScalaZ tutorial and now I'm on the section of Yes-No type class. Eventual goal is to get 1.truthy to return true. Here is the implementation of the typeclass:

trait CanTruthy[A] { self =>
  /** @return true, if `a` is truthy. */
  def truthys(a: A): Boolean
}
object CanTruthy {
  def apply[A](implicit ev: CanTruthy[A]): CanTruthy[A] = ev
  def truthys[A](f: A => Boolean): CanTruthy[A] = new CanTruthy[A] {
    def truthys(a: A): Boolean = f(a)
  }
}
trait CanTruthyOps[A] {
  def self: A
  implicit def F: CanTruthy[A]
  final def truthy: Boolean = F.truthys(self)
}
object ToCanIsTruthyOps {
  implicit def toCanIsTruthyOps[A](v: A)(implicit ev: CanTruthy[A]) =
    new CanTruthyOps[A] {
      def self = v
      implicit def F: CanTruthy[A] = ev
    }
}

implicit val intCanTruthy: CanTruthy[Int] = CanTruthy.truthys({
         case 0 => false
         case _ => true
       })

Looks a little scary to me. We introduced 2 new traits to achieve that. But we can achieve the same just by using implicit classes:

trait CanTruthy {
  def truthy: Boolean
}

object CanTruthy{
  implicit class CanTruthyInt(i: Int) extends CanTruthy{
    override def truthy: Boolean = i match {
      case 0 => false
      case _ => true
    }
  }
}

Looks the same to me. So why do we need to use the way in the tutorial? What kind of things I missed? Can you explain what is the difference?

Upvotes: 2

Views: 77

Answers (1)

Travis Brown
Travis Brown

Reputation: 139038

I think the problem here is a misreading of the scope of this sentence:

Eventual goal is to get 1.truthy to return true.

This is what we're trying to do with the CanTruthyOps stuff, but it's not the goal of the CanTruthy type class, and more generally syntactic concerns like this aren't the goal of type classes.

The goal of type classes is to allow us to constrain types in a simple, flexible, compositional way. The type parameter-less CanTruthy approach doesn't really support the simple part or the flexible part or the compositional part very nicely (arguably the implementation of type classes in Scala isn't very simple either, but it's at least a little simpler and definitely more flexible and compositional).

Take this method from the tutorial, for example (lightly modified to avoid the Any):

// Type class style
def truthyIf[A: CanTruthy, B](cond: A)(ifyes: => B)(ifno: => B): B =
  if (cond.truthy) ifyes else ifno

If you wanted to translate this into your type parameter-less style, at first things seem pretty good:

// Parameterless style
def truthyIf[B](cond: CanTruthy)(ifyes: => B)(ifno: => B): B =
  if (cond.truthy) ifyes else ifno

But now suppose that you needed to keep the original type around. There are lots of reasons this might be necessary—you might want to sort a collection of values with scala.Ordering before you check the truthiness of one of them, for example, or you might have a variation of this method where the original type is also the return type (in the type class style here):

// Type class style
def truthyOrElse[A: CanTruthy](cond: A)(ifno: => A): A =
  if (cond.truthy) cond else ifno

Now the translation is less fun:

// Type parameter-less style
def truthyOrElse[A <% CanTruthy](cond: A)(ifno: => A): A =
  if (cond.truthy) ifyes else ifno

Where the funky <% thing is syntactic sugar for an implicit parameter:

// Type parameter-less style (desugared)
def truthyOrElse[A](cond: A)(ifno: => A)(implicit evidence$1: A => CanTruthy): A =
  if (cond.truthy) cond else ifno

But the : in the type class style is also syntactic sugar:

// Type class style, desugared
def truthyOrElse[A](cond: A)(ifno: => A)(implicit evidence$2: CanTruthy[A]): A =
  if (cond.truthy) cond else ifno

Note that these methods look almost identical—in both you're writing a method that requires some implicit evidence (at compilation time) that A is truthy. In the type parameter-less style this evidence is an implicit conversion, while in the type class style it's an implicit value of a generic type.

There are several advantages to the latter approach. One kind of abstract one is that it allows us to separate the "here's some evidence that I know how to do X for this type" concern from the purely syntactic "I can call .x on this thing" concern. Sure, this separation requires some extra machinery (two traits instead of one), but keeping a clean line between the syntactic and semantic issues is arguably worth it.

Another (related) advantage is that the type class can be more efficient, since it allows us to forgo the syntax, and therefore also the extra allocation it involves:

// Type class style, no syntax
def truthyOrElse[A](cond: A)(ifno: => A)(implicit ev: CanTruthy[A]): A =
  if (ev.truthys(cond)) cond else ifno

Another advantage comes up in cases where the operation you're trying to provide evidence about involves more than one value:

trait Addable[A] {
  def plus(a: A, b: A): A
}

object Addable {
  implicit val intAddable: Addable[Int] = new Addable[Int] {
    def plus(a: Int, b: Int): Int = a + b
  }
}

There's just no nice way to do this kind of thing as an Int => Addable implicit conversion.

The type class approach similarly handles cases where you have multiple types that you need your operation to work on, etc., while the type parameter-less approach doesn't really (at least not in any reasonably clean way).

So to sum up: if you just want some nice enrichment methods that you're generally using in situations where you have concrete types, the type parameter-less approach is totally reasonable and may involve a little less code. If you want to be able to abstract over types that support some operation in an efficient, flexible, generic, and reasonably elegant way, write a type class.

Upvotes: 1

Related Questions