[Proposal]: Implicit parameters #8866
Replies: 53 comments
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
-
Linking to: #3475 Something like that can be done using this feature but it doesn't save much as you would need to spell out each type in local function signature. |
Beta Was this translation helpful? Give feedback.
-
One thing to consider regarding resolution by type: would it make sense to forbid implicit parameters of "primitive" types like For a type with well-defined meaning like But for a primitive type like |
Beta Was this translation helpful? Give feedback.
-
I absolutely see your point @svick. Making primitive or "common use" types implicit is definitely a misuse of implicit parameters in most cases. Although, should we always forcefully prevent writing code that goes against our preferred patterns? Having an implicit If we want to consider some measures, here is a couple of ideas:
My sense is that we won't prevent people from shooting themselves in the foot. What we can do is require such shots to be intentional -- which in my opinion is the case with Possible use of |
Beta Was this translation helpful? Give feedback.
-
That wouldn't work,
I don't think it's going to be clear that void MyFunction(implicit int timeoutSeconds)
{
MyOtherFunction();
SomebodyElsesFunction();
}
void MyOtherFunction(implicit int timeoutSeconds);
// timeout is in milliseconds
void SomebodyElsesFunction(implicit int timeout); And even more so when |
Beta Was this translation helpful? Give feedback.
-
Does scala do anything to prevent "implicit int"? Perhaps we should require that implicits match not just on type, but also on parameter name? This would prevent improper matching of one implicit int to another unrelated one. However, it would mean if I had something like Downside would be at component boundaries if the names did not match. E.g. some components using the name |
Beta Was this translation helpful? Give feedback.
-
Why? It may be considered abusive but it seems unnecessary to denylist an arbitrary set of types. A developer has to opt-in to using this feature anyway, if they want to use implicits that don't make sense (or at least don't make sense to us here) I don't think there's a reason to prevent them.
No.
Scala also doesn't do this.
IMO it would just make the feature that much more brittle than it already is. If someone wanted to pass around a bunch of implicit flags like that they could define a carrier struct and avoid the ambiguity. |
Beta Was this translation helpful? Give feedback.
-
It sounds like the only safe usage is about passing CancellationToken which is already solved with analyzers. In any other case, why would you want to actually HIDE that something is passed as some kind of "context"? (Right now the only implicit parameter is |
Beta Was this translation helpful? Give feedback.
-
I really like how this is handled in Koka. It's a bit more complicated than that, since effect handlers can define multiple values, functions and control statements, but if we restrict this to just values, then something like Data FetchData(implicit CancellationToken token)
{
...
} must either be called from a function that bubbles up that implicit (has as with (var token = GetTheTokenFromSomewhere()) {
var data = ValidateAndFetchData(request);
//ValidateAndFetchData is implicitly passed the token and implicitly passes the token to FetchData()
ProcessData(data); //Again, the same token is passed to ProcessData();
} Of course, this is much more concise in Koka, where you can give a short name to your effects, so something like this could work: implicit parameters CT
{
CancellationToken token
}
Data FetchData()[CT]
{
... // token is in scope here
}
Data ValidateAndFetchData(Request request)[CT]
{
...
return FetchData();
}
with (CT as { token = GetTheTokenFromSomewhere() }) {
var data = ValidateAndFetchData(request);
//ValidateAndFetchData is implicitly passed the token and implicitly passes the token to FetchData()
ProcessData(data); //Again, the same token is passed to ProcessData();
} |
Beta Was this translation helpful? Give feedback.
-
@alrz it's more or less in the same boat with dynamic scoping or associated types. Explicit context passing is good, but noisy. |
Beta Was this translation helpful? Give feedback.
-
I'd agree that it's noisy but I'm concerned on how this works out in practice. Looking at some places that may use this feature: All these are candidates for an implicit parameter, but you will need to depend on IDE to find usages in case any kind of change is required. Immediate compiler feedback helps but since its completely hidden at the call-site, it's prone to abuse and can result in surprises whether you're writing or reading code with implicit parameters all over the place. |
Beta Was this translation helpful? Give feedback.
-
If only implicit parameters were allowed to be passed implicitly, I think it could mitigate the surprise factor. void M0(implicit object o) => Use(o);
void M1(implicit object o) => M0(); // ok
void M2(object o) => M0(); // error |
Beta Was this translation helpful? Give feedback.
-
This is a key constraint of the design. You still need to explicitly indicate that some stuff is handled implicitly. |
Beta Was this translation helpful? Give feedback.
-
I was reading the original proposal and I noticed a difference between how it is written and what I'm hoping for. When it comes to implicit variables, the proposal indicates that they PUSH to an enclosed call site but what I am looking for is a declaration that lets me PULL values at all call sites. Also, in my use case, when something is declared as implicit, it is considered optional as long as it is optional at the site. If this is a method parameter, it is optional if it is an optional parameter. If it is a property, it is optional unless it is a required property. |
Beta Was this translation helpful? Give feedback.
-
Not sure what the difference is. If there is an |
Beta Was this translation helpful? Give feedback.
-
I spent a couple years doing Scala development. Scala has implicit parameters. All implicit parameters can be explicitly provided. Scala even allowed for multiple parameter groups. You would make one group of parameters be explicit, and put your implicits into a second parameter group. It was a nightmare! I am all for creating tools and adding more automation to code, but my years of Scala experience has told me that you can't trust the vast majority of developers with this kind of power. Both the good and bad uses of implicits in Scala made the code an order of magnitude more difficult to understand and debug. I don't recommend this feature. |
Beta Was this translation helpful? Give feedback.
-
@ ClaytonHunsinger I would hope that, if some kind of implicit parameter feature was developed for C#, the lessons would have been learned from Scala's feature (in much the same way that Scala themselves did going from 2 to 3). |
Beta Was this translation helpful? Give feedback.
-
They "were so preoccupied with whether or not they could, that they didn't stop to think if they should..." |
Beta Was this translation helpful? Give feedback.
-
@brownbonnie rather than vague aphorisms, it would be good to post your actual opinion. I will point out that the latest LDM meeting on this topic was not generally positive, and put this in the backlog. |
Beta Was this translation helpful? Give feedback.
-
@333fred Apologies, just trying to add a bit of lighthearted humour to a pretty serious thread :) (It was a Jurassic Park quote) @ClaytonHunsinger explained it well above, implicit params have been a well known pain point in Scala historically. So it would be a case of weighing up the value of this addition, vs the potential misuse and confusion. |
Beta Was this translation helpful? Give feedback.
-
Yes, I know. And aphorisms are fine general, but I would recommend having them accompanied by an actual explanation for those who don't know the quote or how it applies here. |
Beta Was this translation helpful? Give feedback.
-
@333fred Noted thanks :) |
Beta Was this translation helpful? Give feedback.
-
So firstly it makes code harder to read as there are hidden things so when reading it's hard to know where things have come from. Secondly, it can be difficult to know where it is coming from, what happens if you have clashes etc, all just worse development experience all around. |
Beta Was this translation helpful? Give feedback.
-
I don't really like the idea of this feature, but I was thinking about how it could work. How about doing it in a way similar to using?
The idea being anything under the "default" scope, becomes the default instead of whatever default the method had before was. |
Beta Was this translation helpful? Give feedback.
-
This is a very neat idea, and I can see both the benefit (since I, too, have had to deal with APIs where the same three parameters get passed over and over) and the readability hazard. I also really like @Richiban's thought about how it could be used with operator overloads, simply because operator overload methods (and custom type conversions, which also use the Mind you, that doesn't end up being a big issue most of the time, but the lack of ability to specify additional arguments to an operator means that operator overloads can't be used to implement any behavior that requires more than two inputs, even if operator notation would be the most obvious, most write-friendly, and most read-friendly format. As an example, I'll start with modular arithmetic. While the strict mathematical definition only defines a single operator, the ternary congruence operator Today, if I wanted to use an integer as an hour indicator (let's say a 24h indicator, to avoid messy 12/0 problems), I could write a However, if I want to use this struct/role for a sci-fi space exploration sim where each planet has a different number of hours per day, I'm forced to abandon operator syntax entirely and just use methods, or alternately wrap each Second example: what is equality? If I write a wrapper/role for One final example: C# had to add an entirely new syntax to support user-defined To my eyes, this should not be a question of "is it useful to pass context to operator methods?" The answer to that is "yes, and C# already does so in one particular case, in a roundabout way". The question should be "is there a useful, non-ambiguous syntax that would allow developers to declare a context for operators and method calls without handing out loaded footguns?" In answer to that, I'll suggest the following, derived from the existing syntax of int windClockForward(Hour currentHour, int hoursToWind, Planet localPlanet)
{
// Syntax 1: explicit type, name, and value declaration
implicit (int hoursPerDay = localPlanet.HoursPerDay)
{
return currentHour + hoursToWind;
}
// Also syntax 1: multiple types, multiple variables per type
implicit (int hoursPerDay = localPlanet.HoursPerDay)
implicit (bool isChecked = true, notifyWatchers = true)
{
return currentHour + hoursToWind;
}
// Syntax 2a: type, name, and value forwarding
int hoursPerDay = localPlanet.HoursPerDay;
bool isChecked = true, notifyWatchers = true;
implicit (hoursPerDay, isChecked, notifyWatchers)
{
return currentHour + hoursToWind;
}
// Syntax 2b: name and value forwarding with type conversion
int? hoursPerDay = localPlanet.HoursPerDay;
implicit ((int)hoursPerDay)
{
return currentHour + hoursToWind;
}
// Syntax error: HoursPerDay is not specified in any overload called within its scope
implicit (int HoursPerDay = localPlanet.HoursPerDay)
{
return currentHour + hoursToWind;
}
// Calls the second overload
return currentHour + hoursToWind;
}
role Hour for int
{
public Hour Add(int hoursToAdd, implicit int hoursPerDay, implicit bool isChecked = false, implicit bool notifyWatchers = false)
// Alternate syntax placement, works for both expression-bodied and block-bodied members
implicit (hoursPerDay, isChecked, notifyWatchers)
=> this + hoursToAdd;
public static Hour operator+(Hour left, int right, implicit int hoursPerDay, implicit bool isChecked = false, implicit bool notifyWatchers = false)
{
int result = (int)left + right;
if (result >= hoursPerDay && notifyWatchers) PerformWatcherNotify(result);
if ((result < 0 || result >= hoursPerDay) && isChecked) throw new OverflowException();
return (result % hoursPerDay + hoursPerDay) % hoursPerDay;
}
// Ensure that a stray + doesn't hit this Hour if the context isn't set
[Obsolete]
public static Hour operator+(Hour left, int right)
{
throw new NotSupportedException("Needs hoursPerDay in context");
}
} As far as semantics goes, my inclination would be:
|
Beta Was this translation helpful? Give feedback.
-
@MadsTorgersen I think you're still the champion for this - do you or @radrow have any thoughts on my formulation above? |
Beta Was this translation helpful? Give feedback.
-
That's elaborate! You brought some very good points here. Touching on the operators, while the modulo arithmetic example sounds a bit superfluous (I think I would rather have a struct for ints in the "Modulo Land"), I completely agree on the One difference I see between steering int hour = 7;
implicit (int hoursPerDay = localPlanet.HoursPerDay)
{
hour += 21;
}
implicit (int hoursPerDay = localPlanet.Moons()[0].HoursPerDay)
{
// I quite don't like this. `hour` used to "live" in localPlanet,
// but now it was dragged "to the moon"
hour += 30;
} However, strings are just UTF16 arrays regardless of which strategy is taken for comparison. Because of that, implicit parameterization applies "correctly" as solution to your second example, as it is the operation that is tweaked, not the domain. At least, this is how I view it.
Regarding your mention of I think the syntax you've shown is very clear. Since we have one-line implicit int hoursPerDat = localPlanet.HoursPerDay;
return currentHour + hoursToWind; Additionally, it may make sense to allow combining using implicit (var context = new Context()) { ... }
// or maybe?
using (implicit var context = new Context()) { ... } I like your proposal for the resolution. The description seems a bit convoluted as a wall of text, but it feels very intuitive and simple after understanding. I appreciate that you considered ambiguity and mentioned treatment of unused arguments. I do not see any immediate flaws in what you presented. Regarding forwarding implicitness of implicit parameters, I think it might not be necessary if you use the single-line declarations I mentioned above. At least for the start it would not hurt to write public Hour Add(int hoursToAdd, implicit int hoursPerDay, implicit bool isChecked = false, implicit bool notifyWatchers = false)
{
implicit var hoursPerDay = hoursPerDay;
implicit var isChecked = isChecked;
implicit var notifyWatchers = notifyWatchers;
...
} or maybe even public Hour Add(int hoursToAdd, implicit int hoursPerDay, implicit bool isChecked = false, implicit bool notifyWatchers = false)
{
implicit hoursPerDay;
implicit isChecked;
implicit notifyWatchers;
...
} Although, the I have no strong opinions on the |
Beta Was this translation helpful? Give feedback.
-
Thanks for your feedback! Yes, I agree about this being equally useful for methods and for operators; while most of my post talks about operators, I consider the use cases very close to identical. Also, I agree that the first example is a bit contrived - normally I'd like to store modulo bases with the number they're attached to and prevent accidental conversion between bases, too. (That said, I can still see use cases for the implicit-base pattern, like if you're using an "alarm clock" asset made by someone else in your game and it can only store an You're right that, given the one-line Especially since, yes, it makes perfect sense to combine As for your last example about forwarding implicit state, my gut reaction was to say "I already specified that" in the "Syntax 2a" example, but that was before I realized that (a) that's for the multi-line form of the statement, and (b) from the pattern established by public Hour Add(int hoursToAdd, implicit int hoursPerDay, implicit bool isChecked = false, implicit bool notifyWatchers = false)
{
implicit var hoursPerDay, isChecked, notifyWatchers;
...
} And, while thinking about it, I'd personally be inclined to allow a programmer to use deconstruction form to declare implicit arguments of multiple types, in both the one-line and multi-line syntaxes: implicit var (hoursPerDay, isChecked, notifyWatchers) = (24, true, false);
// OR
implicit (var (hoursPerDay, isChecked, notifyWatchers) = (24, true, false))
{
// ...
} I'd only allow the implicit (int hoursPerDay is ambiguous. Is that the start of a multi-line declaration of implicit Of course, this syntax wouldn't be compatible with the |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Implicit Parameters
Summary
This proposal introduces implicit parameters for C#. It is highly inspired by a feature of Scala known under this exact name, as well as similar solutions from other languages. In spite of that inspiration, the aim is to make the new functionality as simple as possible in order to avoid numerous flaws caused by overly complex Scala's design. The motivation is to increase clarity and comfort of writing and refactoring code that extensively passes environmental values through the stack.
Implicit parameters are syntactic sugar for function applications. The idea is to pass selected arguments to methods without necessarily mentioning them across the code, especially where it would be repetitive and unavoidable. Thus, instead of writing
One could simplify it to something like:
Note that the cancellation token (
token
) is provided implicitly to every call that declares it as itsimplicit
argument. While it still needs to be declared in the function signature, the application is handled automatically as long as there is a matching implicit value in the context.A way to look at this feature is that it is a counterpart of the default parameters that are already a part of C#. In both concepts some arguments are supplied by the compiler instead of the programmer. The difference is where those arguments come from; to find the source of an implicit parameter you need to look at the calling function's signature, as opposed to the called function in case of the default parameters.
Motivation
Since it is just a "smart" syntactic sugar, this feature does not provide any new control flows or semantics that were not achievable before. What it offers is that it lets one write code in a certain style more conveniently, and with less of boilerplate.
The promoted paradigm is to handle environment and state by passing it through stack, instead of keeping them in global variables. There are numerous benefits of designing applications this way; most notably the ease of parallelization, test isolation, environment mocking, and broader control over method dependencies and side effects. This can play crucial role in big systems handling numerous tasks in parallel, where context separation is an important security and sanity factor.
Simple
CancellationToken
examples like the previous one are likely to be common. The following example is more elaborate, showing a realistic implementation of a gRPC server converting image files:The code is a lot lighter and arguably cleaner than what it would look like if it passed around
ctx
,inStream
andoutStream
explicitly every time. The code focuses on the main logic without bringing up the contextual dependencies, which are mentioned only in method headers. To show the impact, I marked all the places where the implicit application happens with a// !
comment.Implicit parameters ease refactoring in some cases. Let us imagine that it turns out that
RequestNoFile
needs to check for cancellation, and therefore requiresServerCallContext
to get access to the token:Because in the presented snippet
RequestNoFile
is called only from scopes withServerCallContext
provided, no other changes in the code are required. In contrast, without implicit parameters, every single call toRequestNoFile
would have to be updated. Of course, if the calling context does not have that variable, it needs to get it anyway -- but if it does so implicitly as well, this benefit propagates further. This nicely reduces the complexity of adding new dependencies to routines.Detailed design
General syntax
Since the implicit parameters appear similar to optional parameters, it feels natural to declare them in a similar manner:
Regarding placement, it makes sense to mingle both kinds of special parameters together. Parameters could be also simultaneously implicit and optional as well:
Supplying implicit arguments from non-implicit methods
In order to avoid the mess known from Scala 2, there should always be a clear way of finding the values provided as implicit parameters. Therefore, I propose letting them be taken only:
implicit
local variables (and only local)Hence this:
If supplying them manually starts getting annoying, then a possible workaround would be to lift the context with another method. So this:
turns into this:
Overloading
Resolution rules for overloading should be no different that those for optional parameters. When a method is picked based on the contex,t and there is no suitable implicit parameter in scope, it should result in an error.
Nested functions
In most common cases there should be no reason to prevent local functions from using implicit parameters of enclosing methods. Though, there are two exceptions where it would not work:
A workaround for the former is to declare the static function with the same implicit parameter. That also gives a reason to have a casual shadowing regarding the latter case.
Resolution of multiple implicit parameters
The design must consider ambiguities that emerge from use of multiple implicit parameters. Since they are not explicitly identified by the programmer, there must be a clear and deterministic way of telling what variables are supplied and in what order. A common way of tackling this is to enforce every implicit parameter have a distinct type and do the resolution based on that. It is a rare case that one would need multiple implicit parameters of the same type, and if so a wrapper class or a collection can be used (even a tuple).
There is a special case when inheritance is taken into account, as it can lead to ambiguities:
This should result in an error, ideally poining to all variables that participate in the dilemma. However, as long as the resolution is deterministic, there should be no issue with that. A workaround in such situations is explicit application:
If that feels doubtful, it could be a configurable warning that an implicit parameter is affected by subtyping.
Backwards compatibility
Since I propose reusing an existing keyword, all valid identifiers shall remain valid. The only added syntax is an optional sort of parameters, which does interfere with any current constructs, so no conflicts would arise from that either. There is also no new semantics associated with not using this feature. Thus, full backward compatibility.
Since there is a general convention to keep contextual parameters last anyway, transition of common libraries to use implicit parameters should be quite painless. That is because implicit parameters can still be used as positional ones, so the following codes shall run perfectly the same:
and
...and of course
Performance
These parameters turn into normal ones in an early phase of the compilation, thus no runtime overhead at all. Compilation time would be affected obviously, but it depends on the resolution algorithm. If kept simple (what I believe should an achievable goal), the impact should not be very noticeable. More than that, there is no overhead if the feature is not used.
Editor support
Since the feature would be desugared quite early, it should be easy to retrieve what arguments are applied implicitly. Thus, if some users find it confusing, I believe it would not be very hard to have a VS (Code) extension that would inform about the details of the implicit application. A similar thing to adding parameter names to method calls.
Drawbacks
Well, "implicit". This word is sometimes enough to bring doubts and protests. As much as I personally like moving stuff behind the scenes, I definitely see reasons to be careful. All that implicit magic is a double-edged sword -- on one hand it helps keeping the code tidy, but on the other can lead to nasty surprices and overall degraded readability.
One of the most common accusations against Scala is the so-called "implicit hell", which is caused by sometimes overused combination of extension classes (known there as, of course, "implicit" classes), implicit parameters and implicit conversions. I am not a very experienced Scala programmer, but I do remember finding Akka (a Scala library that uses implicits extensively) quite hard to learn because of that.
As mentioned before, there is an article by Scala itself, that points out flaws in the Scala 2 design. I encourage the curious reader for a lecture on how not to do it.
Also, there is a discussion under a non-successful proposal for adding this to Rust. The languages and their priorities are fairly different, but the critics there clearly have a point.
Alternatives
Resolution by name
Implicit parameters could be resolved by name instead of types. It allows implicit parameters to share type and solves all issues with inheritance, since types wouldn't play any role here. Although, it reduces flexibility since the parameters would be tied to the same name across all the flow of the code. This may slightly harden refactoring. A counterargument to that is that each implicit parameter should generally describe the same thing everywhere, so keeping the same name feels natural anyway and looks like a good pattern that might be worth enforcing.
Local implicit variables
To ease resolution and reduce the amount of code, some local variables could be declared as
implicit
as well. To avoid Scala 2 mess, it is important to allow this solely for method-local parameters and nothing more.Unresolved questions
Design meetings
Beta Was this translation helpful? Give feedback.
All reactions