Update: For the full proposal visit here. It’s different than the proposal originally posted here, mainly due to addition of dedicated type class syntax and the new component for declaring implicit interpretations – “lenses”.
Motivation
This (pre) SIP is a direct follow-up to the updated implicits proposal. I feel that the previous SIP doesn’t accomplish its main goal – making implicits clearer while retaining their usability. If anything, it makes things worse.
I believe it’s not that the proposal has specific problematic parts which sum up to a big problem, but rather that the whole approach is misguided. It views all of the design patterns achieved today with implicits as different forms of “term inference”; dependency injection, extension methods, type classes and implicit conversions.
Making this abstract association between those designs by using a common construct is a major source of confusion surrounding these features. It is hardly a necessity for a developer to understand the underlying connection between these designs.
At its core, this SIP takes the polar opposite approach and wishes to to break this abstraction and give each design its own unique set of constructs. Call a spade a spade; or as we say in Hebrew, call a child by his name.
Dependency injection & Type Classes
Implicit parameters are at the core of two major design patterns; dependency injection and type-classes. Both patterns are a form of the separation-of-concerns principle, where the difference between them lies in the usage of type-parameterized (generic) parameters in the latter but not in the former.
The original wording – implicit
– is quite successful in capturing the idea of this feature, but it conflates two different aspects of the mechanism; (a) declaring where a function may accept an implicit value and (b) the action of making a value implicit and thus available to those functions. If I’m not mistaken, this is the only keyword in the language that has multiple different meanings.
This polysemy is evident not just in the core mechanic of implicits, but rather it’s reflected in its usage of the keyword in all of the “implicit design patterns” as a definition construct, which make it seem as if all of those patterns are strongly related, where in reality they are not.
Thus, it would be fitting to use a different wording that will (a) remain close to the original wording, (b) still capture the essence of the mechanism and – most importantly – (c) will distinguish between the definition site and the call site.
Implication
The mechanism should be thought of as a cloud residing in a certain scope, from which all the values are automatically applied to functions invoked in said scope. This is the “implication cloud” of the scope.
In order to add a value to that cloud, one has to imply
it. Implying a value is an active operation on that value; it is not a defining property of that value. It is an operation with (local) side effects.
In order to make parameters of a function able to accept values from the implication cloud (where the function is called), one has to define them as implied
.
Defining parameters as implied
also automatically places them in the cloud of the function’s scope, and thus propagation of these implied parameters throughout nested invocations is done with minimal syntax.
The result is something fairly similar to what we’re used to with the basic usage of implicits:
// dependency injection
object Future {
def apply[A](body: => A)(implied ec: ExecutionContext) = ???
}
imply ExecutionContext.global
Future { ... }
// type class
trait Ordering[A] { ... }
object Ordering {
object IntOrdering extends Ordering[Int] { ... }
}
imply Ordering._
(2,1,3).sorted
Only parameters are implied
It is possible to imply
any instance available in the language; however, it is impossible to define anything other than parameters as implied
– not class
, object
, def
, val
nor var
.
Previously, defining these “things” as implicit
served only two use-cases; (a) adding values to the implication cloud in a local scope; and (b) forcing these things into the implication cloud of any importing scope.
The first use-case is now replaced with imply
, which is anonymous and accepts any expression:
imply 2
imply otherVariable
imply function(arguments)
imply new Ordering[Int] { ... } // obviously without `new` if anonymous classes change syntax
The second use-case – forcing values into the implication cloud on import
– is quite problematic. It uses a construct (implicit
) in the place of a modifier, but it serves as an operative keyword; it is not an intrinsic property of a “thing”, but is rather an operation with side effects.
It may very well be that the prevalence of this use-case increased due to conflation between the definition site and the call site that implicit
has. With the new syntax, it becomes apparent that this use-case is odd:
imply object IntOrdering extends Ordering[Int] { ... }
Type-classes should be defined just like any other object
; if one wishes to emphasize their usage as a type-class, one should either state it in their documentation or annotate them with @typeclass[Ordering]
(perhaps a new standard annotation?).
It may still be highly useful to provide a shortcut for the two underlying operations of using type-classes:
import imply Ordering._
import imply Ordering.IntOrdering
import Ordering.{imply IntOrdering}
Unlike the previous SIP, I believe that this mechanism of “implicit import” should only serve as a shortcut for these two operations, and shouldn’t provide a new namespace. Modularity and encapsulation can be easily achieved with other constructs:
object Ordering {
private val secret = ???
object IntOrdering extends IntOrdering[Int] { ... } // uses `secret`
}
import imply Ordering._ // implies `IntOrdering`, but not `secret` (as it is private)
Context bound
The following is not a direct continuity to the previous laid out line of thought, and therefore a more “optional” idea in this SIP - that is, dropping the context bounds (by deprecation first, of course).
Context bounds are perhaps not the major source of confusion surrounding implicits, but their irregularity does add up to that confusion, while they only serve as a shortcut that can be expressed with existing constructs (implied
parameter with a single type-parameter).
What makes them irregular?
- They only work with a single type-parameter; any type-class with more than one type-parameter cannot be expressed via context bounds.
- The
:
syntax makes it seems as if the generic type is the type class. This is so because the upper and lower type bounds use similar syntax (<:
,>:
). - They can be expressed alternatively, while the other (type) bounds cannot. It make it seem as if all bounds are somehow related to implicits.
Context bounds also allow for anonymous implied parameters, which can be potentially expressed in other ways:
def foo[A](arg: Int)(implied _: Ordering[A], _: Eql[A, A]) = ???
Dropping context bounds also means that we can probably go away with implicitly
/ summon
.
Extension methods
I wasn’t here in the early days of Scala, but I understand that in some way implicit parameters were introduced to the language as an abstraction of extension methods (and conversions). Regardless, I believe that extension methods are quite different from implicit parameters, and should be completely separated from that concept.
For starters, extension methods don’t have anything to do with functions or parameters. They are all about extending objects in a different way than inheritance. The same thing could be said about type-classes, which may be seen as a form of ad hoc polymorphism, but they are more general in the sense that they can extend multiple types (with more than one type parameter), while extension methods cannot.
Moreover, extension methods are part of a “magical class” – an extension – which is constructed invisibly and tied to an (extended) instance. One might think of this behind-the-scenes instance as “implied”, but this has nothing to do with the “implication cloud” of implicit parameters; in no way the extended instance declared that it accepts implied methods from the implication cloud.
Lastly, extensions are a unique “thing” in the language. They are not an implied class
, object
, def
, etc. Their semantics differ from those of other existing constructs; they are constructed (instantiated) differently and have a special “self” member (referring to the extended instance). Type-classes do not have any such special semantics, but are rather a pattern of using the regular “things” differently.
Therefore, extension methods deserve their own unique syntax:
extension Pretty extends Any {
// `this` is the extended instance
def prettyToString() = s"Pretty: ${this.toString()}"
}
1.prettyToString()
trait RandomSelection[A] {
def random(): A
}
extension SeqOps[A] extends Seq[A] with RandomSelection[A] {
override def random(): A = ???
}
Seq(1,2).random()
Since an extension has no defined constructor, it cannot accept parameters. This reflects the notion that extensions should not maintain a state (they really shouldn’t), and are not a replacement for delegation / decoration patterns. That also means that context bounds are illegal on the type-parameters of the extension (but not on the methods themselves).
Learning from past mistakes, extensions should probably not have their own bounds (like the old view bounds); they should not be used to implement type-classes, nor to define the interface of function. Furthermore, chaining extensions (extension extending another extension) probably should also be disallowed.
Implicit conversions
Lastly, we have implicit conversions; or rather, type conversions. They too differ from implied parameters. Much like extensions, they are also confused as being “implied” because their mechanism works behind the scenes, but are in fact a “thing” with its own special semantics:
conversion intToDouble(i: Int): Double = ???
It is important to remember that conversions are not extensions. The latter extends an object and can refer to it after it has been constructed, while the former cannot.
There is also the topic of chaining conversions, which I have no strong opinion about, and can remain the same as far as I’m concerned.