Eugene Zhulkov
Eugene Zhulkov

Reputation: 515

Whether to use context bound or implicit ev in Scala

According to the style guide - is there a rule of thumb what one should use for typeclasses in Scala - context bound or implicit ev notation?

These two examples do the same

Context bound has more concise function signature, but requires val evaluation with implicitly call:

def empty[T: Monoid, M[_] : Monad]: M[T] = {
    val M = implicitly[Monad[M]]
    val T = implicitly[Monoid[T]]
    M.point(T.zero)
}

The implicit ev approach automatically inserts typeclasses into function parameters but pollutes method signature:

def empty[T, M[_]](implicit T: Monoid[T], M: Monad[M]): M[T] = {
  M.point(T.zero)
}

Most of the libraries I've checked (e.g. "com.typesafe.play" %% "play-json" % "2.6.2") use implicit ev

What are you using and why?

Upvotes: 4

Views: 995

Answers (4)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149538

One caveat you need to be aware of when working with implicitly is when using dependently typed functions. I'll quote from the book "The type astronauts guide to shapeless". It looks at the Last type class from Shapeless which retrieves the last type of an HList:

package shapeless.ops.hlist

trait Last[L <: HList] {
  type Out
  def apply(in: L): Out
}

And says:

The implicitly method from scala.Predef has this behaviour (this behavior means losing the inner type member information). Compare the type of an instance of Last summoned with implicitly:

implicitly[Last[String :: Int :: HNil]]
res6: shapeless.ops.hlist.Last[shapeless.::[String,shapeless
      .::[Int,shapeless.HNil]]] = shapeless.ops.hlist$Last$$anon$34@20bd5df0

to the type of an instance summoned with Last.apply:

Last[String :: Int :: HNil]
res7: shapeless.ops.hlist.Last[shapeless.::[String,shapeless
      .::[Int,shapeless.HNil]]]{type Out = Int} = shapeless.ops.hlist$Last$$anon$34@4ac2f6f

The type summoned by implicitly has no Out type member, that is an important caveat and generally why you would use the summoner pattern which doesn't use context bounds and implicitly.


Other than that, generally I find that it is a matter of style. Yes, implicitly might slightly increase compile times, but if you have an implicit rich application you'll most likely not "feel" the difference between the two at compile time.

And on a more personal note, sometimes writing implicitly[M[T]] feels "uglier" than making the method signature a bit longer, and might be clearer to the reader when you declare the implicit explicitly with a named field.

Upvotes: 3

Oleg Pyzhcov
Oleg Pyzhcov

Reputation: 7353

FP libraries usually give you syntax extensions for typeclasses:

import scalaz._, Scalaz._
def empty[T: Monoid, M[_]: Monad]: M[T] = mzero[T].point[M]

I use this style as much as possible. This gives me syntax consistent with standard library methods and also lets me write for-comprehensions over generic Functors / Monads


If not possible, I use special apply on companion object:

import cats._, implicits._ // no mzero in cats
def empty[T: Monoid, M[_]: Monad]: M[T] = Monoid[T].empty.pure[M]

I use simulacrum to provide these for my own typeclasses.


I resort to implicit ev syntax for cases where context bound is not enough (e.g. multiple type parameters)

Upvotes: 1

Gabriele Petronella
Gabriele Petronella

Reputation: 108101

This is very opinion-based, but one pratical reason for using an implicit parameter list directly is that you perform fewer implicit searches.

When you do

def empty[T: Monoid, M[_] : Monad]: M[T] = {
  val M = implicitly[Monad[M]]
  val T = implicitly[Monoid[T]]
  M.point(T.zero)
}

this gets desugared by the compiler into

def empty[T, M[_]](implicit ev1: Monoid[T], ev2: Monad[M]): M[T] = {
  val M = implicitly[Monad[M]]
  val T = implicitly[Monoid[T]]
  M.point(T.zero)
}

so now the implicitly method needs to do another implicit search to find ev1 and ev2 in scope.

It's very unlikely that this has a noticeable runtime overhead, but it may affect your compile time performance in some cases.

If instead you do

def empty[T, M[_]](implicit T: Monoid[T], M: Monad[M]): M[T] =
  M.point(T.zero)

you're directly accessing M and T from the first implicit search.

Also (and this is my personal opinion) I prefer the body to be shorter, at the price of some boilerplate in the signature.

Most libraries I know that make heavy use of implicit parameters use this style whenever they need to access the instance, so I guess I simply became more familiar with the notation.


Bonus, if you decide for the context bound anyway, it's usually a good idea to provide an apply method on the typeclass that searches for the implicit instance. This allows you to write

def empty[T: Monoid, M[_]: Monad]: M[T] = {
  Monad[M].point(Monoid[T].zero)
}

More info on this technique here: https://blog.buildo.io/elegant-retrieval-of-type-class-instances-in-scala-32a524bbd0a7

Upvotes: 3

Frederic A.
Frederic A.

Reputation: 3514

Note that on top of doing the same, your 2 examples are the same. Context bounds is just syntactic sugar for adding implicit parameters.

I am being opportunistic, using context bound as much as I can i.e., when I don't already have implicit function parameters. When I already have some, it is impossible to use context bound and I have no other choice but adding to the implicit parameter list.

Note that you don't need to define vals as you did, this works just fine (but I think you should go for what makes the code easier to read):

def empty[T: Monoid, M[_] : Monad]: M[T] = {
  implicitly[Monad[M]].point(implicitly[Monoid[T]].zero)
}

Upvotes: 1

Related Questions