Implicit vs Scala 3's Given
I don’t like given
, as an alternative to implicit
in given
and implicit
, that I hope is fair…
This article uses Scala 3.1.2
, which is the most recent version at the time of writing. I just played around, and may have an incomplete understanding — so instead of doing thorough research, I prefer to complain, in order to have others jump in and correct my misunderstandings 😜 Take this with a grain of salt.
Evaluation #
Let’s define a type-safe alias for representing email addresses, and we’d like to implement cats.Show and cats.Eq.
In Scala 2.13 we’d define the “type-class instances” in the class’s companion object, like so:
package data
import cats._
import cats.syntax.all._
final case class EmailAddress(value: String)
object EmailAddress {
implicit val show: Show[EmailAddress] = {
println("Initializing Show")
v => v.value
}
implicit val equals: Eq[EmailAddress] = {
println("Initializing Eq")
(x, y) => x.value === y.value
}
}
If we’d like to use these instances, they are available in the “global scope”, so we can do:
package main // another package
import cats.syntax.all._
import data.EmailAddress
object Main extends App {
val email = EmailAddress("noreply@alexn.org")
println("------")
println(s"Show: ${email.show}")
println(s"Is equal to itself: ${email === email}")
println("\nMemoized?\n-------")
println(s"show: ${implicitly[Show[EmailAddress]] == implicitly[Show[EmailAddress]]}")
println(s"equals: ${implicitly[Eq[EmailAddress]] == implicitly[Eq[EmailAddress]]}")
println()
}
Which would print:
Initializing Show
Initializing Eq
------
Show: noreply@alexn.org
Is equal to itself: true
Memoized?
-------
show: true
equals: true
Let’s switch to given
, which should replace the implicit
flag on these instances. In the official Scala 3 documentation, the given
definitions are given outside the companion object, like so:
//...
final case class EmailAddress(value: String)
given show: Show[EmailAddress] = {
println("Initializing Show")
v => v.value
}
given equals: Eq[EmailAddress] = {
println("Initializing Eq")
(x, y) => x.value === y.value
}
This is because in Scala 3 the “package objects” don’t need syntax, so you can just dump such definitions in a file. There’s just one problem — these instances are no longer global, so when we try compiling our project, the compiler now says:
[error] |no implicit argument of type cats.Show[example.EmailAddress] was found for parameter e of method implicitly in object Predef
[error] |
[error] |The following import might fix the problem:
[error] |
[error] | import example.show
Well, there is one thing about Scala 3 that I love here: the errors on missing implicits are awesome, because they suggest the possible imports ❤️
But if you want global visibility, and you should, you still have to place them in a companion object; so the official documentation is a little confusing right now.
//...
final case class EmailAddress(value: String)
object EmailAddress {
given show: Show[EmailAddress] = {
println("Initializing Show")
v => v.value
}
given equals: Eq[EmailAddress] = {
println("Initializing Eq")
(x, y) => x.value === y.value
}
}
Now it works, but there’s a difference:
------
Initializing Show
Initialized: noreply@alexn.org
Initializing Eq
Equal to itself: true
Memoized?
-------
show: true
equals: true
These given
instances are defined as lazy val
, in fact. The documentation even has this sample:
given global: ExecutionContext = ForkJoinPool()
Kind of makes sense for this to be lazily evaluated, but consider how you’d define this value in Scala 2.x:
implicit lazy val global: ExecutionContext = ForkJoinPool()
To me, in a strictly evaluated language like Scala, this definition is much clearer, whereas the given
definition “complects” storage / evaluation, which to me is a separate concern. In a language like Scala, how our values get initialized is a pretty big problem that we always care about.
But wait, is a given
always a lazy val
? What if we add a type parameter?
final case class EmailAddress(value: String)
object EmailAddress {
//...
// Forcing a type parameter that doesn't do anything:
given equals[T <: EmailAddress]: Eq[T] = {
println("Initializing Eq")
(x, y) => x.value === y.value
}
}
As you’re probably going to guess, the answer is no — adding a
simple type parameter turns this given
definition into a def
,
so now we get this output:
------
Initializing Show
Initialized: noreply@alexn.org
Initializing Eq
Equal to itself: true
Memoized?
-------
show: true
Initializing Eq
Initializing Eq
equals: false
This isn’t obvious at all, because in fact the object reference
for this Eq
instance could be reused, this being perfectly safe:
object EmailAddress {
//...
given equals[T <: EmailAddress]: Eq[T] =
// This cast is perfectly safe due to
// contra-variance of function parameters 😉
eqRef.asInstanceOf[Eq[T]]
private val eqRef: Eq[EmailAddress] = {
println("Initializing Eq")
(x, y) => x.value === y.value
}
}
Well, compare and contrast with usage of implicit
:
implicit def equals[T <: EmailAddress]: Eq[T]
Which signature is easier to read?
But what if we don’t want the lazy val
? What if we always want the behavior of a def
? The lazy val
implies synchronization behavior that we may want to avoid.
Scala 3 supports an inline
keyword, which is pretty cool, and does what you’d expect:
//...
object EmailAddress {
//...
inline given equals: Eq[EmailAddress] = {
println("Initializing Eq")
(x, y) => x.value === y.value
}
}
But that’s not quite the same as a def
. Inlining functions is cool, sometimes, for performance reasons, but other times the effects are unpredictable and should be used with care.
Compare and contrast:
implicit def equals: Eq[EmailAddress]
In this context, the only advantage of using given
is in eliminating the name of those values:
object EmailAddress {
given Show[EmailAddress] =
v => v.value
given Eq[EmailAddress] =
(x, y) => x.value === y.value
}
Sure, this looks cool. Kind of a high price to pay though. And there’s more …
Final #
Consider this example:
final case class TypeInfo[T](
typeName: String,
packageName: String,
)
trait Newtype[Src] {
opaque type Type = Src
implicit val typeInfo: TypeInfo[Type] = {
val raw = getClass
TypeInfo(
typeName = raw.getSimpleName.replaceFirst("[$]$", ""),
packageName = raw.getPackageName,
)
}
}
object FullName extends Newtype[String] {
// Override, as the default logic may not be suitable;
override implicit val typeInfo =
TypeInfo(
typeName = "FullName",
packageName = "example",
)
}
I’m using this same pattern in monix/newtypes, this being an instance in which an implicit value is provided by a trait, but we leave the possibility of overriding it, and for good reasons.
What if we’d use given
?
trait Newtype[Src] {
opaque type Type = Src
given typeInfo: TypeInfo[Type] = {
val raw = getClass
TypeInfo(
typeName = raw.getSimpleName.replaceFirst("[$]$", ""),
packageName = raw.getPackageName,
)
}
}
Well, that doesn’t work, because the declared given
is a final
member, therefore we can no longer override it:
[error] -- [E164] Declaration Error: ...
[error] 22 | override implicit val typeInfo =
[error] | ^
[error] |error overriding given instance typeInfo in trait Newtype of type example.TypeInfo[example.FullName.Type];
[error] | value typeInfo of type example.TypeInfo[example.FullName.Type] cannot override final member given instance typeInfo in trait Newtype
So finally, this:
given typeInfo: TypeInfo[Type]
Is actually equivalent with this:
implicit final lazy val typeInfo: TypeInfo[Type]
Yikes; that’s not obvious 😏
Vocabulary #
The documentation can definitely improve, but I don’t think the documentation is the problem.
As a vocabulary preference, I don’t see how given
makes things easier to understand, especially for beginners, but this is a subjective opinion, and it may be a matter of learned taste. It’s exactly the same concept, though, using given
and using
does not make things easier to understand, unless the power of implicits is limited, implicits being hard to understand due to their power, not due to their naming. And I don’t see how beginners could be given an explanation that doesn’t use the term “implicit parameters”, something that “givens” obscures. But I concede that I may lack imagination for teaching 🤷♂️
I will argue that the existence of both implicit
and given
introduces more Perl-like TIMTOWTDI, but in a bad way (I’m saying this as a guy that loves TIMTOWTDI, usually). If given
is the future, I’d like to say that implicit
should be deprecated, but given the current behavior of given
, I hope implicit
stays 🤨
Does given
have any redeeming quality that I’m not seeing? Maybe any good design reasons for why it forces the current behavior?