-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
An Alternative to Implicits #5458
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Add" instead of "Added")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
178bd77
to
25b84a9
Compare
Reading through, at a first glance this seems like a mostly sideways change that isn't clearly better or worse than the status quo, and for something with such a huge & clear migration cost it should have correspondingly huge & clear benefits. It's not clear to me that the proposed syntax/semantics significantly helps solve the problems stated up front:
There's some interesting bits and pieces in here (e.g. anonymous witnesses/implicits) but overall I think the proposal doesn't make a sufficiently good case about why exactly this, and not something else, is the correct-enough solution for a big-enough problem that is worthy of the huge syntactic change over the status quo. For example, while the implicit def [From, To](implicit c: Convertible[From, To]): Convertible[List[From], List[To]] = new {
def convert (this x: List[From]): List[To] = x.map(c.convert)
} Or for the implicit val IntOrd: Ord[Int] = new {
def compareTo(this x: Int)(y: Int) =
if (x < y) -1 else if (x > y) +1 else 0
}
implicit def ListOrd[T: Ord]: Ord[List[T]] = new {
def compareTo(this xs: List[T])(ys: List[T]): Int = (xs, ys) match {
case (Nil, Nil) => 0
case (Nil, _) => -1
case (_, Nil) => +1
case (x :: xs1, y :: ys1) =>
val fst = x.compareTo(y)
if (fst != 0) fst else xs1.compareTo(ys1)
}
} If we could find similar ways to get most of the other benefits of this proposal with small, standalone changes that are themselves valuable on their own, to me that would be preferable over a big-bang rewrite-everything migration. I suspect we can. Some ideas to make the proposal better:
|
I understand @lihaoyi feedbacks, and I believe his stated 3 points are a good trail to help the proposal keep going forward. |
I dropped the dependency injection section. It is now mostly confusing and does not convey a new point. There was a point once to it, in an earlier design (before b86e91d) where the two roles of an implicit parameters of (1) taking implicit arguments, and (2) being itself a witness for further implicits were separated. Then the example was much nicer. It was left in, to show that it's in principle still possible to do this even if the two parameter roles are not separated. But without that earlier design |
Just wanted to say, as someone who "gets" implicits, this is much harder for me to understand than implicits. The only problem with implicits (problem being that they are difficult for newcomers to grasp) is that the term is applied to two different things – implicit conversions (I call these "non-explicit conversions") and actual implicits (facts, derivations, and bounds). Newcomers conflate these things and end up thinking all implicits are bad, because implicit conversions are bad (I've heard so many people call typeclasses "implicit conversions"). But it is the actual (good) implicits that are getting hosed here. It seems backwards to me that the good parts have to change rather than the bad parts. Wouldn't it be simpler to just change the syntax for implicit conversions? Like maybe you have to say Another thought: Just because something takes a little bit of understanding doesn't make it bad. I think a lot of the hysteria (for lack of a better word) around implicits in Scala is misplaced and is better solved with education than with trying to radically redesign it. Once people learn about it they generally come to understand that it's a really elegant mechanism for abstracting over many elements of the type system in terms of "theorem proving" (in a lightweight sense). But without removing that power, or removing the elegant abstraction, you can't change the fact that it requires a little bit of a paradigm shift for a career Java programmer to understand. And they have to want to do that – some people won't, and that ought to be OK. |
And one more comment, sorry: Solving some of the ergonomics around implicits would get a lot further than changing the name to escape its baggage. A better story around user-friendly error reporting for failed proofs would solve 80% of the complaints. A story around coherence and orphans would solve probably a further 15% (if such a story is possible). The remaining tail of complaints typically come from people who complain about "magic" (but then will happily use runtime-reflection driven dependency injection stuff like Guice...) |
I am going to have to go through this in a bit more detail but I am having a hard time getting convinced about what the improvement witness's actually bring, kind of echoing @lihaoyi points but I am have more of my own which I am going to list. With your example of the def currentMap(implicit ctx: Context): Map[String, Int] This can easily be solved by simply stating that all currentMap.implicitly(someContext)
// or this version which can also solve the currying issues that Scala has with implicits
currentMap("someKey").implicitly(someContext)
This would allow you to do implicit val ctx: Context = ???
currentMap("someKey") As desired. The proposed syntax also has a lot of consistency issues, I am actually for more making the current Regarding dependency injection, this can already be done naturally with implicits and traits currently. class UserClient(implicit connectionDetails: ConnectionDetails, scheduler: Scheduler) {
...
}
class AddressClient(implicit connectionDetails: ConnectionDetails, scheduler: Scheduler) {
...
}
trait ServicesConfig {
lazy val userClient = ???
lazy val addressClient = ???
}
trait DatabaseConfig {
implicit lazy val connectionDetails = ConnectionDetails("localhost",8080)
}
trait SchedulerConfig {
implicit lazy val scheduler = Scheduler(executionContext)
}
object Main extends App with ServicesConfig with DatabaseConfig with SchedulerConfig {
logger.info("Initialized")
} And you can do the thing that you actually want to do with dependency injection, which is replacing specific "nodes" of your dependency graph, i.e. lazy val testApp = new ServicesConfig with DatabaseConfig with SchedulerConfig {
override lazy val userClient = MockUserClient(....)
} EDIT: I realized that the DI section was dropped but I will still leave this here to demonstrate that the current implicit's solve the DI problem fine. Also in regards to syntax, we really shouldn't overload the meanings of keywords. Although claiming that Scala has fewer keywords than other languages is a notable goal it shouldn't be at the cost of giving keywords overloaded meanings. For example, Note I am not really saying that the witness is pointless, but I would rather not throw away the baby with the bathwater and fix the current issues we have with implicit's and then evaluate if we need witness and how much value it adds. For example claiming that current implicits are problematic because the compiler gives bad information when implicits are missing, honestly this to me sounds like a scapegoat. There is nothing stopping the compiler from giving detailed warnings proviso performance issues and if thats the case I don't see how witness's would be any better here. Also I would consider removing implicit conversions (which iiirc are currently deprecated) or severely limit them, this is the only concrete issue I have seen with implicit abuse in the Scala ecosystem |
That's a good point. Witnesses are at best a "sideways" move for someone who gets implicits, which is everyone in this discussion. But my focus is, what about somebody who does not get implicits because they are new to Scala? Programs should express meaning and intent. Implicits express mechanism instead. An eye opener for me was when I worked on extension methods (which are by themselves a huge improvement for expressing typeclasses) and was left with
Really? That seemed like a sore thumb sticking out. Doing some language archeology, the word Fast forward 12 years. By now, most uses of implicit conversions are frowned upon and the language will make it ever harder to define and use them. But the word It's really easy to rewrite automatically, so I am not worried about migration at all. In terms of breaking code I am not worried either, since old implicits will be kept around for a while, then be deprecated for a while, before they would be finally removed in 3.2 or 3.3. It's true that a lot of books and other material will have to be rewritten. But let's face it: Much of the material in these books dealing with implicits would benefit from a rewrite. Many treatments that I have read (or written!) start with implicit conversions, which by now is quite cringe-worthy! |
The feedback I get from others indicates this. It seems to be the #1 problem with implicits.
This is interesting. I did not see it that way. There are languages (i.e. Rust, and, to a lesser degree Swift) built around extension methods to do everything! Why do you think they are a problem? Is it that a missing import causes obscure errors? We could try to come up with solutions that address this problem separately.
True. That seems to be the one usage of implicit conversions that carries its weight: Define them with that target type to which they convert.
It's completely orthogonal. Witnesses are a fairly simple syntactic and type transformation. The most important thing is that they force alignment of parameter definitions and arguments.
This adds up! Meaning instead of mechanism...
No, since it is not implicit. Implicit is a misnomer. I have to jump on a plane, so I have to leave it at that. |
Maybe As for |
I suppose Go also can be thought of as using extension methods for everything, and they seem to not have a problem. The only difference i can think of is that in Go/Rust/Swift, extension methods aren't "first class" IIRC: you can only define them top-level and import them from top-level packages, and can't do clever tricks like defining them in a Scala is one of the only language which allows this, because most languages don't let you import from arbirary values. In fact, Scala explicitly prohibits defining extension methods top-level in a file like most other languages! Perhaps that is what causes confusion in Scala?
This seems like using
I'd argue that the idea of
Having parameters or conversions be implicits is not that foreign a concept. On the other hand, the idea of a Consider Googling programming implicit
programming witness
By replacing "implicit" with "witness", to the vast majority of programmers you are replacing a probably-seen-before-in-similar-concept key word with the name of a video game. You can't get much more misnomer than that! It's worth bringing up the question: what is people's real problem with implicits in Scala? People get by just fine with extension methods in Rust, Go or Swift, and people get by just fine with implicit conversions/constructors in C++ or C#: maybe you don't like them or use them all the time, but they don't cause the average person too much pain using the language. You can very much use Scala like C#: with implicit constructors to save on boilerplate and extension methods to tweak the syntax, once in a while. Why then do implicit parameters and implicit conversions get a bad rap in Scala? I'd argue the problem isn't so much implicits themselves, but the usage of implicits in the community. This starts, first and foremost, in the standard library: collections and To solve this, the solution would be to build a core ecosystem that uses implicits in a "good way", get it out there into the community, popularize it, and get people to build upon that both for their own happiness and as example code to learn from:
If we can, then at least we'll have figured out what the "good" ways of using implicits are, and therefore we would like to encourage via language tweaks. And if we don't even know what it means to build "good" implicit-using library code ourselves, there's no way we'd be able to make the right decision on how to tweak the language to make other people improve their usage of implicits! |
The problem is implicit scope. I am surprised that is not mentioned more prominently in these comments. Implicit scope is hard to visualise. For the user of an implicit there always seems to be one too many places to look for the definition. For the author there is always a niggling concern that the implicit might not be available when it should or that an implicit might escape and affect something it shouldn't. Different libraries have different conventions that the user should follow for bringing necessary implicits into scope. I don't think users of rust traits, for example, have these mental speed bumps. |
``` | ||
Context bounds in witness definitions also translate to implicit parameters, and therefore they can be represented alternatively as with clauses. For instance, here is an equivalent definition of the `ListOrd` witness: | ||
```scala | ||
witness ListOrd[T] with (ord: Ord[T]) for List[Ord[T]] { ... } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it should be Ord[List[T]]
instead of List[Ord[T]]
.
An underscore ‘_’ can be used as the name of a required witness, if that witness does not | ||
need to be referred to directly. For instance, the last `ListOrd` witness could also have been written like this: | ||
```scala | ||
witness ListOrd[T] with (_: Ord[T]) for List[Ord[T]] { ... } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it should be Ord[List[T]]
instead of List[Ord[T]]
. (save as above)
I agree that the word "witness" seems inadequate, outside of academia. Why not "capability"? After all, that's what implicit parameters really are. It works well with extension methods: capability Foo {
def foo(this x: T): U
} I'd say that "capability" is also a better name than "type class" (which is quite terrible, because "class" already means something else in Scala). The following makes perfect sense to me: def foo(x: T)(capability s: Show[T]) = ... So first-class capabilities are the distinguishing feature of Scala, not implicits, even though capabilities are usually (but not always!) passed around implicitly. Other languages have various forms of implicits, but no mainstream language has first-class capabilities. Also, as @arnolddevos says, it does seem like implicit scope (first-class capability scope) seems to be the most problematic thing that people complain about. |
@odersky I feel like this is going in the right direction, I completely agree with the problem analysis of what is inconsistent and confusing about current implicit syntax and think the new proposal provides a neater syntax that describes the intent more clearly. The sole concern for me, which seems to be shared by some commenters here, is the choice of name (witness), which I find mildly confusing. I might be missing something from common cs terminology, but for me a witness is something rather passive, where the action had been somewhere else and the witness merely testifies that, e.g. in an implicit witness parameter that just testifies that A is indeed a subtype of B. But in this case a witness does not just testify that an instance for type class My mind is in a lazy Sunday haze so I sadly do not have a counter-proposal for a name and instead would just like to ask: could you describe why you think this is a good choice for a name? Have you considered any alternative names that you later discarded? Are there examples from other languages where this term is used in a similar way? The only other question is: from the four reasons you give in the motivation section, three are addressed by the proposal in a pretty obvious way. The only exception seems to be tooling. If there is an obvious way this proposal makes smarter tooling easier, it might be nice to explicitly describe the advantage. |
@jenshalm I arrived at "witness" after a some iterations. I had "extension"and "instance" before, but both looked too generic. I don't know. Judging from Twitter, people doing type classes like "witness" and it seems quite clear to them. Other languages are not much better: |
(Still need to read properly into the proposal, but based mainly on the conversation --)
I can sort of see that, but honestly -- I'm not at all sure how to teach that to novice Scala programmers in a way that they will actually understand. I kind of like "implicit" because I've figured out over the years how to teach it: I describe a "cloud" of implicit definitions around the code. The This comment kind of worries me in that respect. The concepts of "type" vs. "term" rarely come up in introductory Scala courses -- I don't think we even get into that in our advanced ones. And the notion of program-as-proof rarely comes up. On-the-ground programmers generally don't care about this stuff, and I avoid it rigorously while teaching -- it's the sort of topic that drives many folks to think of Scala as "too hard"... |
Can one create a class with context parameters? class Context
object Context extends Context
class MyClass(name: String) with (ctx: Context) In this case it's quite hard to tell what new MyClass("name") with Context // looks like inheritance |
I would be in favor of dropping |
I believe implicits or witnesses is not a subject you want to teach to a novice programmer anyway! |
Honestly, I don't think there's much choice. Even a fairly straightforward concept like Ordering requires basic typeclasses, and doing anything with No, it's not something I teach in the first few days. But in practice, as soon as an engineer hits a non-trivial codebase they will hit implicits, and the more curious ones (especially the experienced programmers -- I'm not talking about new programmers, I'm talking about new Scala programmers) get really annoyed if you try to brush them off as something they shouldn't worry their little heads about. I don't go deeply into implicits at that point, mind. But being able to provide some intuition about what is going on, in terms that they can grok, is pretty important fairly early on, in my experience... |
I'm a big supporter of rethinking I think the parameter list representation of implicits is indeed one of the hardest parts to understand, and it's also one of the hardest usages of the keyword to avoid in basic real-world Scala programming. Consider the pervasive need to pass ExecutionContext if you're using standard library I disagree with the idea that implicit is familiar to users of other languages. My experience elsewhere is almost totally limited to implicit conversions, which is often not what people do in idiomatic Scala. I'd argue that this association actually makes Scala's system more difficult to grasp, because the analogy doesn't go very far.
Related to that question, I do think it could be very helpful to separate the conveyance of implicits down the call stack from their summoning. For conveyance, complete anonymity is preferable. For summoning, you don't necessarily need to convert the implicit any further. It could be very helpful to separate these concerns in a way that could be combined, but only if necessary. The context bound sugar does a great job of this in that particular use case of implicit. As an aside, React's context is actually more implicit, in the literal sense, than Scala implicits, because you don't need to declare it anywhere between the provision and consumption, unlike Scala's use of implicit parameter lists. Would this be possible to achieve? In this case, conveyance could literally be implicit. |
@jducoeur @acjay @LPTK I agree implicit parameters come up immediately when teaching some libraries. One of the first times I mention implicits in my courses is Implicit objects and classes? I believe that's comparatively rarer, and it's a harder concept to grasp, for the following reason: Type classes are a second order concept - they are types that describe properties of other types, which may or may not hold depending on context. I.e.
That idea takes some getting used to but I believe it is essential to be clear about this if one wants to teach type classes. My earlier, abandoned attempt at extensions was in part motivated by the wish to delay the introduction of type classes by modelling context dependent extensions instead. That approach introduced other complexities; in particular it required additional machinery for talking about non-extension methods such as But once one introduces type classes as properties of types, I believe
The other problem with The last (maybe decisive?) problem is that it is longer than "witness" and has 5 syllables instead of two. So there's that... |
The other thing I like about |
I think what this expands to is partly a question of how good the compiler is at inlining things enough to get rid of allocations. When it defaults to minting new instances all over the place, there's an argument that we need the manual control to make things objects or vals to reduce allocations. Manually control the flyweighting. If the compiler (with appropriate hints) can inline enough to get rid of most allocations then I'm very relaxed about everything being defs. I tend to weigh in on the side of simpler language, more complex compiler --- but then I don't take responsibility for the compiler.
|
By the way, if It would be much easier for beginners to see the correspondence between implicit parameter sections and implicit function types: def foo(x: Int) with (y: Foo): Bar = ...
val foo2: (x: Int) => with (y: Foo) => Bar = foo
// equivalent to:
val foo: Int => with Foo => Bar |
I'm going to merge this now, with the proviso that it being merged doesn't represent endorsement by, or preemption of, the SIP committee. It's a large and controversial proposal which will be discussed in detail by the committee and there's absolutely no guarantee that it will appear in Scala 3. Speaking only for myself, I share some of the concerns raised above: whilst the concision gained for type class declarations and instances is welcome, the cost in terms of syntactic churn is very high. It's not clear to me yet that the price is worth paying. My attitude to implicits has evolved significantly over the last few years, and will most likely evolve even more over the next few. What I've come to realize is that implicit resolution is to terms as type inference is to types. Or to put it another way implicit resolution is term inference. This is very explicit in languages like Coq, Agda, Idris and Lean. Inference though, whether of types or of terms, is orthogonal to typing, and I'm finding it increasingly difficult to see why there should be a difference in typing between any of the following definitions, def foo(i: Int): Int = i+i
def bar(implicit i: Int): Int = i+i
implicit def baz(i: Int): Int = i+i All three are methods which take an I think it's something like this observation which motivates the comment made several times above that "implicit arguments are just arguments". I think those comments are almost spot on. Why should a marker that relates to elaboration change an argument from being an ordinary argument to being something else, when the semantics of the definition in question, and the semantics of the elaborated term are otherwise unchanged? The direction this takes me is that I'd now love to see elaboration behaviour separated out from method/function signatures and other definitions altogether (so, arguments are just ordinary arguments) and I think we should try and come up with some alternative mechanism for specifying how elaboration should happen and how programmers can influence it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment prior to merge ...
I agree that we still need to discuss this. The analogy between type inference and term inference is spot on I think. I believe from that analogy follows that our syntax must make it clear where a term is missing, so that it can be inferred. Meanwhile, I was having some new thoughts how to make the proposal less controversial. But that will be a different PR. |
Can you link to it here so people subscribed to this ticket can follow the
new one?
…On Tue, Jan 29, 2019 at 11:56 AM odersky ***@***.***> wrote:
I agree that we still need to discuss this. The analogy between type
inference and term inference is spot on I think. I believe form that
analogy follows that our syntax must make it clear where a term is missing,
so that it can be inferred.
Meanwhile, I was having some new thoughts how to make the proposal less
controversial. But that will be a different PR.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#5458 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAGAUHSe22qvEGW6Lig41lIqJCjVzwF4ks5vIH1CgaJpZM4Ymu8J>
.
|
Using |
It does make class files slightly larger but I don't think it will introduce a sizeable runtime overhead since conversions can be inlined. |
@ltpk @b-studios I am experimenting with a tweak which uses |
@odersky Great, I would be happy to look into this as soon as you have a draft. Besides the syntactic details, I really love the separation of passing arguments from passing implicit arguments (explicitly). I quite often use value inference to guide type inference; to (BTW, I guess you were to mention @LPTK :) ) |
@milessabin I think you're entirely right that implicits are term inference, analogous to type inference. I've been frustrated with earlier incarnations of scala that there was not a sufficient syntactic separation between what was being inferred and what had to be explicitly supplied. It leads to problems like needing to put To normalise syntax, perhaps something could be done in the called signatures to flag individual parameters as being ones that are derived, rather than entire parameter lists.
I've found it very difficult to juggle implicit vals, def and objects. No end of bugs and of failed refactorings have been down to me mangling the interchange between these. And the So I'd welcome a unified syntax where // declare a "constant" derivable value
instance MyFoo: Foo = new { ... }
// declare derivables made by a function
instance ListSemigroup[T](instance T: Semigroup[T]): Semigroup[List[T]] = new { ... }
// declare a derivable by a function that uses @FunctionalInterface sugar
instance ListSemigroup[T](instance T: Semigroup[T]): Semigroup[List[T]] =
(_: List[T]).zipWith(_).map { (l, r) => T.append(l, r) } Or lastly, we could use instance ListSemigroup[T](deriving T: Semigroup[T]): Semigroup[List[T]] = ... |
@milessabin Would your earlier comment suggest not labeling arguments as |
@acjay Yes, something along those lines. |
That sounds more elegant to me than the status quo, on some level. I'd guess the vast majority programs have strictly disjoint sets of implicit and non-implicit types. But I could also see how this could be really confusing. Seems worth considering, though! FWIW, I don't see this proposal as turning implicit parameters into non-parameters, at all. They'd simply be declared and provided differently from positional parameters. It would be still be straightforward to map to conventional parameter. Kind of like how |
Evolved version: #5825 |
@odersky isn't the Very contrived illustration: object O {
def overloaded(a: Int)(b: String) = 1
def overloaded(a: Int) = (c: Boolean) => 2
} Arguably this def implicitParam(a: Int)(implicit b: String) = (c: Boolean) => 3 is the same thing: a method that has two ways to invoke it, with two parameter lists or with one. Again, a very intuitive solution is to say that Last time I suggested it you wrote,
I don't see why it's obscure, it's very natural. The fact that right now the parentheses don't do anything was very surprising to me when I first encountered it (a long time ago :) ). To be honest I don't really get why having to write Also,
I really don't see the problem with that, any more than it's a problem with default arguments. Implicits-as-defaults is very elegant, it unifies two overlapping concepts. And if you want you could tackle it for defaults as well, again by parentheses work, as above. |
[This PR was called "Explore Witnesses as an Alternative to Implicits"]. The current iteration replaces
witness
withinstance
. I have therefore changed the wording in the title and description of the PR]Excerpted from the
motivation.md
file in this PR:Scala's implicits are its most distinguished feature. They are the fundamental way to abstract over context. They represent a single concept with an extremely varied number of use cases, among them: implementing type classes, establishing context, dependency injection, expressing capabilities, computing new types and proving relationships between them.
At the same time, implicits are also a controversial feature. I believe there are several reasons for this.
First, being very powerful, implicits are easily over-used and mis-used. This observation holds in almost all cases when we talk about implicit conversions, which, even though conceptually different, share the same syntax with other implicit definitions. For instance,
regarding the two definitions
the first of these is a conditional implicit value, the second an implicit conversion. Conditional implicit values are a cornerstone for expressing type classes, whereas most applications of implicit conversions have turned out to be of dubious value. The problem is that many newcomers to the language start with defining implicit conversions since they are easy to understand and seem powerful and convenient. Scala 3 will put under a language flag both definitions and applications of "undisciplined" implicit conversions between types defined elsewhere. This is a useful step to push back against overuse of implicit conversions. But the problem remains that syntactically, conversions and values just look too similar for comfort.
Second, implicits pose challenges for tooling. The set of available implicits depends on context, so command completion has to take context into account. This is feasible in an IDE but docs like ScalaDoc that are based static web pages can only provide an approximation. Another problem is that failed implicit searches often give very unspecific error messages, in particular if some deeply recursive implicit search has failed. The dotty compiler implements some improvements in this case, but further progress would be desirable.
Third, the syntax of implicit definitions might be a bit too minimal. It consists of a single modifier,
implicit
, that can be attached to a large number of language constructs. A problem with this for newcomers is that it often conveys mechanism better than intent. For instance, a typeclass instance is an implicit object or val if unconditional and an implicit def with implicit parameters if conditional. This describes precisely what the implicit definitions translate to -- just drop theimplicit
modifier, and that's it! But the cues that define intent are rather indirect and can be easily misread, as demonstrated by the definitions ofi1
andi2
above.Fourth, the syntax of implicit parameters has also some shortcomings. It starts with the position of
implicit
as a pseudo-modifier that applies to a whole parameter section instead of a single parameter. This represents an irregular case wrt to the rest of Scala's syntax. Furthermore, while implicit parameters are designated specifically, arguments are not. Passing an argument to an implicit parameter looks like a regular applicationf(arg)
. This is problematic because it means there can be confusion regarding what parameter gets instantiated in a call. For instance, inone cannot write
currentMap("abc")
since the string "abc" is taken as explicit argument to the implicitctx
parameter. One has to writecurrentMap.apply("abc")
instead, which is awkward and irregular. For the same reason, a method definition can only have one implicit parameter section and it must always come last. This restriction not only reduces orthogonality, but also prevents some useful program constructs, such as a method with a regular parameter whose type depends on an implicit value. Finally, it's also a bit annoying that implicit parameters must have a name, even though in many cases that name is never referenced.None of the shortcomings is fatal, after all implicits are very widely used, and many libraries and applications rely on them. But together, they make code using implicits more cumbersome and less clear than it could be.
Can implicit function types help? Implicit function types allow to abstract over implicit parameterization. They are a key part of the program to make as many aspects of methods as possible first class. Implicit function types can avoid much of the repetition in programs that use implicits widely. But they do not directly address the issues mentioned here.
Alternative Design
implicit
is a modifier that gets attached to various constructs. I.e. we talk about implicit vals, defs, objects, parameters, or arguments. This conveys mechanism rather than intent. What is the intent that we want to convey? Ultimately it's "trade types for terms". The programmer specifies a type and the compiler fills in the term matching that type automatically. So the concept we are after would serve to express definitions that provide the canonical instances for certain types.This PR works out this idea in detail. It consists of
The PR is based on #5114
Direct link to docs:
Motivation
Instance Definitions
Context Parameters and Arguments
Replacing Implicits
Discussion