Reputation: 515
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
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
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 Functor
s / Monad
s
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
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
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 val
s 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