During a discussion at work we stumbled into the topic of best/worst features of Scala, and it was suggested that the implicit system was the worst feature.
I disagree with this, it might be misunderstood and misused, but far from worst feature (that has to be the type interference [sic!]). So in order to continue the tradition of misusing it, I'll present a view of implicits that some might not have seen or thought of.
The idea is to elucidate the Curry–Howard correspondence that's present in Scala's type system, and use implicits to automatically construct proofs. That this is possible should not be surprising, but what's interesting is how easy it is.
My motivation for this exercise is that at work we deal a lot more with business logic than with computations. I hear statements such as: "a user has payed for content, therefore he is entitled to use the content" rather than "these are the first million prime numbers", and "P implies Q" rather than numerical properties.
An explicit Curry-Howard correspondence
The Curry-Howard correspondence is the observation that type-systems can be thought of as logics (and vice versa). This sounds far harder than it actually is:
- Propositions correnpond to types
- Implications correspond to functions
- Conjunctions and disjunctions both correspond to simple ADTs (or products and coproducts for the fancy)
- Negations are functions together with a bottom type
Here are some examples motivating this correspondence using Scala and its implicit system.
Propositions
A proposition A
is expressed as a type:
trait A
A proof of proposition A
is expressed as an inhabitant of the type A
:
implicit val proofOfA = new A {}
So we can ask the compiler if it can prove A
using:
implicitly[A]
which would fail with a type-error for unproven propositions:
trait B
illTyped { "implicitly[B]" }
where I've used illTyped
from shapeless
Non-termination
When thinking about computers and proofs, non-termination has to be taken into account, and we can't trust the type system alone.
Imagine that a mathematician says: "I have a proof of A, I just have to write it down!" and then never comes back. Do you trust him? The type system would! Consider this definition:
@tailrec
def diverge[A](): A = diverge[A]()
which makes the expression diverge()
an inhabitant of any type:
:t diverge(): Int
:t diverge(): Nothing
But as sceptical humans we want to see the actual proof, and so does Scala
at runtime. So when we use implicitly
to ask for a proof, it's crucial to wait
for the evaluation to succeed.
Tangent: Exceptions as non-termination
Of course there's a simpler way of inhabiting Nothing
in Scala, and that's to
throw
:
(throw new Exception): Nothing
but the point of using diverge
is that even without resorting to exceptions
it's possible (and easy) to inhabit any type including the bottom type Nothing
.
Tangent: Dependently typed systems
Note that in stronger type systems
more properties can be proven at compile time. For instance Idris
deals with the above example by nothing that diverge
is not total, and therefore rejects proofs that
uses it with the trade-off that you can trust proofs using only total functions.
Implications
An implication is expressed as a function:
implicit def `A implies B`(implicit a: A): B = new B {}
Then if we have a proof of A
in-scope, we can prove B
:
implicit val proofOfA = new A {}
implicitly[B]
and of-course we can't use C => D
to prove D
without a proof of C
:
trait C
trait D
implicit def `C implies D`(implicit c: C): D = new D {}
illTyped { """implicitly[C]""" }
illTyped { """implicitly[D]""" }
Conjunction
Conjunction (or and
) can be expressed using a tuple:
implicit def `P, Q implies (P and Q)`[P, Q](implicit p: P, q: Q): (P, Q) = (p, q)
We can now query if the compiler can prove or conjunction:
implicit val proofOfA = new A {}
implicit val proofOfB = new B {}
implicitly[(A, B)]
and of course when one of the proofs are missing the conjunction can't be proven:
illTyped { """implicitly[(A, C)]""" }
illTyped { """implicitly[(C, B)]""" }
Disjunction
Disjunction (or or
) can be expressed using a simple ADT:
sealed trait Disjunction[T, S]
case class FromT[T, S](t: T) extends Disjunction[T, S]
case class FromS[T, S](s: S) extends Disjunction[T, S]
case class FromTS[T, S](t: T, s: S) extends Disjunction[T, S]
accompanied by some implicits to hook it up:
trait LowPriorityDisjunctionProofs {
implicit def disjunctionT[T, S](implicit t: T): Disjunction[T, S] = FromT(t)
implicit def disjunctionS[T, S](implicit s: S): Disjunction[T, S] = FromS(s)
}
object Disjunction extends LowPriorityDisjunctionProofs {
implicit def disjunctionTS[T, S](implicit t: T, s: S): Disjunction[T, S] = FromTS(t, s)
}
Let's see it in action:
implicit val proofOfA = new A {}
implicit val proofOfB = new B {}
illTyped { "implicitly[C]" }
illTyped { "implicitly[D]" }
implicitly[Disjunction[A, C]]
implicitly[Disjunction[C, B]]
implicitly[Disjunction[A, B]]
illTyped { "implicitly[Disjunction[C, D]]" }
Why not just use Either
?
Good question, let's see what happens:
implicit def disjunctionLeft[T, S](implicit t: T): Either[T, S] = Left(t)
implicit def disjunctionRight[T, S](implicit s: S): Either[T, S] = Right(s)
This seems to work fine at first
implicitly[Either[A, C]]
implicitly[Either[C, B]]
but this breaks:
illTyped { "implicitly[Either[A, B]]" }
with the clue:
<console>:32: error: ambiguous implicit values:
both method disjunctionLeft of type [T, S](implicit t: T)Either[T,S]
and method disjunctionRight of type [T, S](implicit s: S)Either[T,S]
match expected type Either[A,B]
implicitly[Either[A, B]]
^
That is, when Scala finds two ways (with the same priority) of proving an implicit Scala will not automatically choose for you.
This is the reason for FromTS
above, and
the trick with putting the implicit in its companion object and
stacking implicits with lower priorities in traits.
I first encountered this idea in Scalaz
and shapeless, both libraries
are excellent examples of libraries that uses Scala's features to the fullest
and are big inspirational sources.
Negation
To express the negation of a proposition A
one can use the bottom type
Nothing
to represent the false formula, and define:
implicit val notA: A => Nothing = { _ => throw new Exception }
This encoding relies on observing that A
and ¬A
is a contraction:
implicit def `A and ¬A is a contradiction`(implicit a: A, notA: A => Nothing): Nothing = notA(a)
When we get a Nothing
into the implicit system the world becomes quite absurd:
implicit val proofOfA = new A {}
:t implicitly[B]
:t implicitly[Int]
:t implicitly[Nothing]
thankfully the runtime system will not be so lenient and none of
implicitly[B]
implicitly[Int]
implicitly[Nothing]
will terminate (here it'll throw the exception we new
:ed above).
Note that these typeable expressions will correctly be rejected with a fresh REPL:
scala> :t implicitly[Nothing]
<console>:21: error: could not find implicit value for parameter e: Nothing
implicitly[Nothing]
But it is quite fun to sneak really strange expressions past the compiler:
implicit def `Embrace the nothingness`[P, Q](implicit p: P): Q = implicitly[Nothing]
val b: B = new A {}
val i: Int = b
that of course fail at runtime.
Here's the full code
(using diverge instead) for checking that scalac
accepts this absurd world.
Conclusion
Scala's implicit system might be misused and disliked by many. But I think it's one of the more interesting features of Scala, and my preferred view of it is that it's a readily available proof engine embedded into the language.
A down to earth usage that is encountered frequently in the wild are typeclasses
for JSON-codecs.
Since typeclasses are implemented in Scala as implicits, when handed the
evidence that a type A
has a ToJSON
typeclass instance by implicitly
, that
evince can be a constructive proof, a cookbook, of a way to convert a value of type
A
to JSON.
Examples of this can be seen for instance in Argonaut and it's cats based fork circe, and in spray-json with its obligatory companion spray-json-shapeless. (I can't but suspect they are inspired by the excellent aeson library, but I might be biased towards Haskell in general.)