Skip to content
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

[Proposal]: Implicit parameters #6300

Open
1 of 4 tasks
radrow opened this issue Jul 21, 2022 · 53 comments
Open
1 of 4 tasks

[Proposal]: Implicit parameters #6300

radrow opened this issue Jul 21, 2022 · 53 comments

Comments

@radrow
Copy link

radrow commented Jul 21, 2022

Implicit Parameters

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

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

Data FetchData(CancellationToken token) {
    var request  = MakeDataRequest(token);
    var service  = GetService("main", token);
    var response = service.Send(request, token);
    
    if(response.IsNotOk) {
        service = GetService("fallback", token);
        return service.Send(request, token).Data();
    }
    
    return response.Data();
}

MakeDataRequest(CancellationToken token);
GetService(string name, CancellationToken token);

One could simplify it to something like:

Data FetchData(implicit CancellationToken token) {
    var request  = MakeDataRequest();
    var service  = GetService("main");
    var response = service.Send(request);
    
    if(response.IsNotOk) {
        service = GetService("fallback");
        return service.Send(request).Data();
    }
    
    return response.Data();
}

MakeDataRequest(implicit CancellationToken token);
GetService(string name, implicit CancellationToken token);

Note that the cancellation token (token) is provided implicitly to every call that declares it as its implicit 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:

void CheckCancellation(implicit ServerCallContext ctx) =>
    ctx.CancellationToken.ThrowIfCancelled();

bool FileCached(string fileName, implicit ServerCallContext ctx) =>
    ctx.RequestHeaders.Get("use_cache") && Cache.Exists(fileName);

async Task ConvertJpgToPng(
  int fileSize,
  string fileName,
  implicit IAsyncStreamReader<Req> inStream,
  implicit IServerStreamWriter<Res> outStream,
  implicit ServerCallContext ctx)
{
    bool cached = FileCached(fileName); // !
    
    Jpg jpg = null;
    if(cached)
    {
        await RequestNoFile(); // !
        jpg = Cache.Get(fileName); // !
    }
    else
    {
        jpg = await RequestFile(); // !
    }
    CheckCancellation(); // !
    
    Png png = jpg.ToPng();
    
    await outStream.WriteAsync(new Res(){Png = png}); // !
}

async Task RequestNoFile(implicit IServerStreamWriter<Res> outStream) =>
    await outStream.WriteAsync(new Res(){SendFile = false}); // !
    
async Task<Jpg> RequestFile(
  implicit IAsyncStreamReader<Req> inStream,
  implicit IServerStreamWriter<Res> outStream,
  implicit ServerCallContext ctx) {
    await outStream.WriteAsync(new Res(){SendFile = true }); // !
    CheckCancellation(); // !
    Req msg = await inStream.ReadAsync(); // !
    CheckCancellation(); // !
    return msg.Png;
}

The code is a lot lighter and arguably cleaner than what it would look like if it passed around ctx, inStream and outStream 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 requires ServerCallContext to get access to the token:

async Task RequestNoFile(implicit IServerStreamWriter<Res> outStream, implicit ServerCallContext _) {
    await outStream.WriteAsync(new Res(){SendFile = false});
    CheckCancellation();
}

Because in the presented snippet RequestNoFile is called only from scopes with ServerCallContext provided, no other changes in the code are required. In contrast, without implicit parameters, every single call to RequestNoFile 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:

void f(int x, implicit int y, implicit int z) {}

Regarding placement, it makes sense to mingle both kinds of special parameters together. Parameters could be also simultaneously implicit and optional as well:

void f(int x, implicit int y, implicit int z = 3, int w = 4) {}

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:

  • From the implicit parameters of the current method
  • Through explicit application
  • (OPTIONAL) from implicit local variables (and only local)

Hence this:

int f(int x, implicit int y);

int g() {
    return f(3, y: 42);
}

If supplying them manually starts getting annoying, then a possible workaround would be to lift the context with another method. So this:

void f1(implicit int x);
void f2(implicit int x);
void f3(implicit int x);

void g() {
    int arg = 123;
    f1(x: arg);
    f2(x: arg);
    f3(x: arg);
}

turns into this:

void f1(implicit int x);
void f2(implicit int x);
void f3(implicit int x);

void g() {
    int arg = 123;
    gf(x: arg)
}

void gf(implicit int arg) {
    f1();
    f2();
    f3();
}

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:

  • When the nested function is static
  • When the nested function shadows an implicit parameter by declaring another one of the same type

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:

void f(implicit Animal a) {}

void g(implicit Dog d, implicit Cat c) {
    f();  // Which animal should be supplied?
}

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:

void f(implicit Animal a) {}

void g(implicit Dog d, implicit Cat c) {
    f(a: d);  // That's clear
}

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:

// Version 1.0 before implicit parameters
async void Send(Message message, CancellationToken token);

// LegacyEnterpriseSoftwareIncorporated's business logic
async void SendFromString(string s, CancellationToken token)
{
    Send(new Message(s), token);
}

and

// Version 1.1 after implicit parameters
async void Send(Message message, implicit CancellationToken token);

// LegacyEnterpriseSoftwareIncorporated's business logic
async void SendFromString(string s, CancellationToken token)
{
    Send(new Message(s), token);
}

...and of course

// Version 1.1 after implicit parameters
async void Send(Message message, implicit CancellationToken token);

// ModernStartupHardwareFreelance's business logic
async void SendFromString(string s, implicit CancellationToken token)
{
    Send(new Message(s));
}

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.

void f(implicit int x);

void g() {
    implicit int x = 123;
    f();
}

Unresolved questions

  • Should we allow implicit parameters on properties?
  • Should we allow implicit parameters on constructors?
  • Do we want to allow declaring implicit local variables?
  • Resolution by type vs by name.
  • Should we allow passing an implicit parameter when it is of a subtype of the declared one? (assuming no ambiguities)?
  • Should nested functions inherit implicit context from their declarators?

Design meetings

@HaloFour

This comment was marked as resolved.

@radrow

This comment was marked as resolved.

@HaloFour

This comment was marked as resolved.

@alrz
Copy link
Contributor

alrz commented Jul 21, 2022

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.

@svick
Copy link
Contributor

svick commented Jul 22, 2022

One thing to consider regarding resolution by type: would it make sense to forbid implicit parameters of "primitive" types like int and string?

For a type with well-defined meaning like CancellationToken, there is very little space for mistakes or confusion: if a function has an implicit CancellationToken parameter, passing it to every called function that also has an implicit CancellationToken parameter is what you want.

But for a primitive type like int, implicit int by itself does not mean anything. So passing an implicit int parameter to a called function with an implicit int parameter has a high risk of doing the wrong thing.

@radrow
Copy link
Author

radrow commented Jul 22, 2022

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 int is not technically incorrect, it's just hard to imagine a case where it would be beneficial. More than that, with resolution by type the compiler would make it hard to abuse, since there could be maximum one implicit parameter of every such type.

If we want to consider some measures, here is a couple of ideas:

  • Ban implicit parameters on value types (that solves int, but not string)
  • Require some annotation/keyword on classes/structs that can be implicit (pollutes the code imo, requires libraries to adapt)
  • Add warnings for builtin types that we think shouldn't be implicitly passed

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 implicit int.


Possible use of implicit int: education about the feature. Maybe someone wants just to play around and see how implicit parameters behave?

@svick
Copy link
Contributor

svick commented Jul 22, 2022

Ban implicit parameters on value types (that solves int, but not string)

That wouldn't work, CancellationToken is a value type.

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 implicit int.

I don't think it's going to be clear that implicit int is somehow risky, when implicit CancellationToken is going to be the recommended practice. Especially when interacting with code from somebody else. E.g.:

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 SomebodyElsesFunction didn't have an implicit parameter in previous version, so just upgrading it broke my code.

@CyrusNajmabadi
Copy link
Member

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 implicit bool performLogging, then that would work in an entire component for that pattern used everywhere.

Downside would be at component boundaries if the names did not match. E.g. some components using the name token while others use cancellationToken. But perhaps that's ok?

@HaloFour
Copy link
Contributor

@svick

One thing to consider regarding resolution by type: would it make sense to forbid implicit parameters of "primitive" types like int and string?

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.

@CyrusNajmabadi

Does scala do anything to prevent "implicit int"?

No.

Perhaps we should require that implicits match not just on type, but also on parameter name?

Scala also doesn't do this.

However, it would mean if I had something like implicit bool performLogging, then that would work in an entire component for that pattern used everywhere.

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.

@alrz
Copy link
Contributor

alrz commented Jul 22, 2022

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 this and that's pretty fundamental to the language)

@orthoxerox
Copy link

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 implicit CancellationToken token in its signature) or provides it explicitly:

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();
}

@orthoxerox
Copy link

In any other case, why would you want to actually HIDE that something is passed as some kind of "context"?

@alrz it's more or less in the same boat with dynamic scoping or associated types. Explicit context passing is good, but noisy.

@alrz
Copy link
Contributor

alrz commented Jul 22, 2022

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:

https://github.com/dotnet/roslyn/blob/8a31e4d004314c170cb4f85ec946a7e40deef46c/src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs#L162-L165

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.

@alrz
Copy link
Contributor

alrz commented Jul 22, 2022

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

@radrow
Copy link
Author

radrow commented Jul 22, 2022

If only implicit parameters were allowed to be passed implicitly

This is a key constraint of the design. You still need to explicitly indicate that some stuff is handled implicitly.

@sab39
Copy link

sab39 commented Jul 22, 2022

One use case for this feature that I would expect to make heavy usage of is the ability to write DI-friendly extension methods.

Until recently I worked on a codebase that relied heavily on ambient context, where I'd commonly use extension methods to facilitate more readable code. For example, foo.Frob().Wrangle() is often much clearer than Frob(Wrangle(foo)), and without extension methods foo?.Frob()?.Wrangle() needs multiple lines or a mess of ternary operators.

The project I work on now uses DI rather than ambient context, which is obviously more in line with modern best practice, but without an ambient context, injected dependencies need to be passed to the extension methods explicitly, which undermines the readability that's the goal of such extension methods in the first place.

With implicit parameters, this would no longer be an issue - as long as you could apply implicit to properties and fields rather than just parameters (and locals):

public interface IPrioritizer
{
  int GetPriority(Operation op);
}
public static class PrioritizationExtensions
{
  public static IEnumerable<Operation> Prioritize(this IEnumerable<Operation> operations, implicit IPrioritizer prioritizer)
    => operations.OrderBy(op => prioritizer.GetPriority(op));
}
public class PrioritizedDispatchService
{
  protected implicit IPrioritizer Prioritizer { get; }
  public DispatchService(IPrioritizer prioritizer)
  {
     this.Prioritizer = prioritizer;
  }
  public void DispatchOperations(IEnumerable<Operation> operations)
  {
    foreach (var op in operations.Prioritize()) // passes this.Prioritizer because of the implicit modifier on the Prioritizer property
    {
      op.Dispatch();
    }
  }
}

One important open question with this approach is whether, for example, Prioritizer would still be considered implicit in a subclass of PrioritizedDispatchService. For my own use cases, it'd be much more convenient if the implicitness were inherited, because one of the main reasons for having a common base class in the first place is to avoid the need for duplicated code when inheriting from it. But I can see a persuasive counterargument that it's confusing and dangerous if the meaning of code can be 'invisibly' changed by the base class without some kind of explicit opt-in.

@iam3yal
Copy link
Contributor

iam3yal commented Jul 24, 2022

@sab39 I don't know how I feel about implicit properties and fields, personally I think that only implicit parameters should be supported but if the LDT are going to decide to extend it to more than just that then it should be limited to the current class, meaning private otherwise it will reduce code readability greatly especially when combined with inheritance.

@MgSam
Copy link

MgSam commented Aug 1, 2022

While typing less is nice and marginally beneficial- code is read far more times than it is written. And there is no doubt that this makes the code more confusing and harder to read. Now, instead of reading a line of code and knowing exactly what is being called, I need to actually rely on an IDE to either F12 to whatever method is actually being resolved or scroll up and check, double check, and triple check what implicit parameters might be in scope.

Code like this would quickly become a nightmare to read when pared with method overloads.

@fifty-six
Copy link
Contributor

Requiring explicit specification of variables that can/will be used implicitly also somewhat matches things like State in Haskell, where you don't have to pass around a context, but it's pretty explicitly specified so you know where it's coming from. I've found that when using it, it's very convenient and it's still clear when the context is being used. Though, a difference in that language is that as it's done with Monad, you can see a/the context is used because you have to very explicitly bind or use do, though I don't know how you'd fit that kind of thing into C#. That's also helped by the lack of void returns, so when you see something used at top level without any binding in a do block you'll know it's using the 'context'.

@Richiban
Copy link

Richiban commented Aug 20, 2022

I don't like the idea of using parameter names as part of the matching algorithm, because IMO it gives parameter names an unwelcome, far-reaching significance. For example, if I have an implicit parameter that I want to rename I have to contend with the fact that parameters will now get renamed all the way up the call stack.

Similarly, I may be blocked from actually using this feature if I want to call methods in two libraries I don't control that haven't given the same name to the parameter:

// Library A
public void F(implicit CancellationToken cancellationToken) {...}

// Library B
public void G(implicit CancellationToken ct) {...}

// My library
public void M(implicit CancellationToken ???) // What name do I give this parameter?
{
    A.F();
    B.G();
}

When talking inspiration from other languages, we should also mention Kotlin's prototype Context Receivers feature. In it, they recommend that context parameters (their terminology for implicit parameters) are of a type that was explicitly designed to be used as such. You're not required to implement a specific interface or anything like that, but it's expected that your implicit parameter would be a type such as CancellationTokenProvider or CancellationContext rather than the CancellationToken itself.

While I agree with the sentiment behind this, I think in C# there are good reasons for wanting this to be as compatible as possible with the various existing method signatures out there in the wild. Still, Kotlin is a language that is well worth keeping an eye on for new language features.

@radrow
Copy link
Author

radrow commented Aug 21, 2022

@Richiban You have a very good point. I think it would be a nice linter rule to enforce certain name convention for implicit-param types, such as what we have for attributes and streams.

@Leemyy
Copy link

Leemyy commented Sep 21, 2022

I can definitely think of times where I would have loved this as a feature.

There is one potentiall pitfall that came to mind, though.
If you start out with code similar to the following:

void e(CancellationToken token);

void f(CancellationToken token);
void f();

void g(CancellationToken token) {
    e(token);
    f();
}

and then upgrade by adding implicit, you could suddenly end up binding to a different overload:

void e(implicit CancellationToken token);

void f(implicit CancellationToken token);
void f();

void g(implicit CancellationToken token) {
    e();
    f(); //now binding to a different overload of f
}

The way I see it, this could be addressed in several ways:

  1. If an ambiguity exists between an overload with an implicit parameter and an overload without, pick the one without the implicit parameter.
  2. Warn on a function definition, if it only differs from another overload by implicit parameters.
  3. Warn on any call site that is ambiguous between overloads that only differ by implicit parameters.

@HaloFour
Copy link
Contributor

@Leemyy

This situation already exists with optional parameters (on which this implicit parameter proposal is based), and the compiler will already prefer the overload without the optional parameter:

SharpLab

using System;

class C {
    static void M() => Console.WriteLine("Without optional");
    static void M(string foo = null) => Console.WriteLine("With optional");
    
    static void Main() {
        M(); // prints Without optional
    }
}

@333fred 333fred added this to the Backlog milestone Sep 26, 2022
@Eli-Black-Work
Copy link

It looks like this has a proposal champion but also has 3x more downvotes than upvotes. Does having a proposal champion mean that this feature is slated for implementation?

@CyrusNajmabadi
Copy link
Member

Does having a proposal champion mean that this feature is slated for implementation?

No. It means there is an LDM member willing to carry it through the design process. This is merely at the proposal stage currently.

@333fred
Copy link
Member

333fred commented Oct 6, 2022

And of note, the most recent time this was brought up in LDM, that negative community sentiment was mentioned.

@Eli-Black-Work
Copy link

Got it, thanks! 🙂

@Richiban
Copy link

I thought I'd talk about a really cool use case for this: in Kotlin "contexts" can also be defined for operators. A domain I'm working in a lot right now is VS Code extensions (I know it's Typescript but the situation would be the same for a C# codebase). It involves a lot with Positions, which is basically a "point" in a text document sense i.e. a line and character number. We should be able to transpose a Position by adding two positions together, such as:

Position p1 = ...
Position p2 = ...

Position result = p1 + p2;

but the problem is that you could easily create a point that isn't valid; such as it's off the end of the given line or the line number is greater than the number of lines in the document. In order to properly add two Positions together you need access to the Document as well, but how will you pass it in? You can't use + any more since it's a binary operator. You can fall back to a method such as p1.Add(p2, document) but that feels like a design smell and it's a shame that we've lost the simple operator form. You could leave the "normalisation" of the point to the Document type, like this:

Position result = p1 + p2;
result = document.Normalise(result);

but it's easy to forget to do this (thus creating a pit of failure). In Kotlin, you can solve this by placing a context receiver on an operator function:

context(DocumentContext)
operator fun Position.plus(other: Position): Position {
    ...
}

and now, our domain is nicely designed in such a way that you are able to add two Positions together in the context of a document:

context(DocumentContext)
fun DoTheThing() {        
    Position p1 = ...
    Position p2 = ...

    Position result = p1 + p2; // Now it is guaranteed that `result` is adjusted to fit within the document
}

This is something that would be very valuable in C# too; I can imagine it working like this:

// Definition
public static Position operator +(Position a, Position b, implicit Document document) 
{
    ...
}

// Use
public void DoTheThing(implicit Document document)
{
    Position p1 = ...
    Position p2 = ...
    
    var result = p1 + p2;
}

So, operators would be allowed to have more than the regular number of parameters but any additional parameters must be implicit.

@CyrusNajmabadi
Copy link
Member

That example is highly worrying to me. It genuinely makes me feel like a free being used to mask bugs. I.e. of them program is trying to manipulate points like this such that it goes out of bounds, then it has done something wrong. And implicitly trying to make that but happen is just hiding a deeper issue.

This is not idle concern for me either. Many many times I've seen in the development of VS components making such mistakes. Generally because they were mixing data inappropriately (like points from one time and snapshots from another; or points from two different times). Having a system implicitly papering over that would lead to masking that instead of failing early and obviously, forcing the underlying problem to be fixed.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Nov 20, 2022

document.Normalise(result);

So we have code in roslyn that does this. But only very rarely, and only with comments explaining why it's the right thing to do. For example, there are times when we may be searching a stale index, whose positions then might not be in the bounds of a file that has since changed. In such a case, we do need to normalize things, but we document that that is both intentional and represents the fact that the feature itself is trying to be "best effort" and this will likely take the user to the right location most of the time, and that we will tell the user that the results are potentially stale, making this an acceptable experience.

@Richiban
Copy link

Sorry Cyrus, but I'm really not seeing how this masks bugs... If you take a Position and say "move two down and five right" the code to adjust that to the document (either by wrapping or truncating) has to be somewhere and I don't see how the syntactic method by which you call the move function has any bearing on bugs at all

@Eli-Black-Work
Copy link

@Richiban I think that's an interesting example, but why not pass document to the Position constructor? One then ends up with "document-relative" positions which seems fine to me:

Position p1 = new Position(10, document);
Position p2 = new Position(15, document);

Position result = p1 + p2;

@CyrusNajmabadi
Copy link
Member

If you take a Position and say "move two down and five right" the code to adjust that to the document

Why are you moving the position to an invalid location? That seems to be a bug in the first place. Masking it doesn't help.

Again, this is not a hypothetical for me. This has happened many times over the time we've been working on VS. Masking out of bounds errors by clamping to what is in position is not a good thing. It just means buggy features are behaving badly.

@En3Tho
Copy link

En3Tho commented Nov 21, 2022

Well, what about a context for UI frameworks or something? For example, take Blazor- there is a RenderTreeBuider type which allows manually building Blazor markup.

Implementing a DSL on top of this would require passing builder around somehow because every piece of rendering logic goes through it. Maybe a contextual parameter can improve visual quality here. Especially now when we got Required properties.

Example with documents is kinda worrying me too because I personally would really strive to avoid creating an invalid point altogether.

This feature overall feels like an another pit of failure to me honestly and I'm like 100% sure people will abuse it to create a mess of unreadable things just because they are lazy to type a few more characters but I admit there are specific cases where this might be cool.

@Richiban
Copy link

Why are you moving the position to an invalid location? That seems to be a bug in the first place

User input: if the user has entered "5↓" or "20→" you have to check at some point whether that's a valid move, and what to do accordingly. I prefer to have the movement wrap around lines and simply stop at the end of the document, because that is what the user would expect. (i.e. if the user wants to go 5 words to the right but there are only 3 words left on the current line, the cursor should wrap aroundn)

Anyway, this is rather besides the point: we're arguing coding styles when I'm just trying to give an example of a domain in which you frequently have to pass a contextual object (the document) to just about every single function. Not only is it an irritation but I thought I'd found a good example of a situation in which none of the current options were particularly great. Never mind.

@CyrusNajmabadi
Copy link
Member

User input: if the user has entered "5↓" or "20→" you have to check at some point whether that's a valid move,

Yes with user input you have to validate. So validate immediately and decide what to do with it. That's very different from just contextually clamping values which would make logic bugs in your own code.

@TonyValenti
Copy link

Hi All, I have a few feature requests regarding implicit parameters:

  1. Allow them to work with properties.
  2. Allow them to be combined with required properties.

Specifically, the following meanings would apply:
required: a value must be explicitly provided
implicit: a value may be explicitly provided, but if not, try to find one that is in scope. If you cant, that is OK.
implicit required: a value may be explicitly provided, but if not, try to that is in scope. If you cant, that is an error.

@radrow
Copy link
Author

radrow commented Mar 18, 2023

"If you cant, that is OK" --- but only if there is a default value provided

@TonyValenti
Copy link

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.

@HaloFour
Copy link
Contributor

@TonyValenti

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.

Not sure what the difference is. If there is an implicit value in scope, it would be considered as the default value for a given implicit parameter. If there is no implicit value in scope, it would either be a compiler error, or default to an optional value as declared on the method.

@ClaytonHunsinger
Copy link

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.

@Richiban
Copy link

@ 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).

@brownbonnie
Copy link

They "were so preoccupied with whether or not they could, that they didn't stop to think if they should..."

@333fred
Copy link
Member

333fred commented Nov 28, 2023

@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.

@brownbonnie
Copy link

@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.

@333fred
Copy link
Member

333fred commented Nov 28, 2023

(It was a Jurassic Park quote)

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.

@brownbonnie
Copy link

@333fred Noted thanks :)

@grab-a-byte
Copy link

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.
I had to work in scala 2 once before and frankly this concept is the reason I never want to work I scala 2 eer again. Seeing it in C# would be a huge step back in my personal opinion.

@wrexbe
Copy link

wrexbe commented Apr 26, 2024

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?

default (var cancel = new CancellationToken())
{
    var request  = MakeDataRequest();
    var service  = GetService("main");
    var response = service.Send(request);

    if(response.IsNotOk) {
        service = GetService("fallback");
        return service.Send(request).Data();
    }

    return response.Data();
};
default var cancel = new CancellationToken();
var request  = MakeDataRequest();
var service  = GetService("main");
var response = service.Send(request);

if(response.IsNotOk) {
    service = GetService("fallback");
    return service.Send(request).Data();
}

return response.Data();

The idea being anything under the "default" scope, becomes the default instead of whatever default the method had before was.

@dmchurch
Copy link

dmchurch commented May 29, 2024

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 operator keyword) are currently quite limited - for example, unlike (to the best of my knowledge) every other method type, they have no way to specify an exact implementation at a callsite - the best you can do is typecast the arguments appropriately and check to make sure the compiler has picked the right one.

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 a ≡ b (mod m), the colloquial understanding of "clock arithmetic" is that every mathematical operator has a ternary, modular equivalent. So, while the strict mathematical translation of "three hours after the clock reads 10, it will read 1" would be "10 plus 3 is congruent with 1 modulo 12", or 10 + 3 ≡ 1 (mod 12), the colloquial understanding is more akin to "10 plus 3, in a modulo 12 environment, is 1".

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 readonly struct to wrap an int and I could write operator overloads for all the arithmetic operators that simply performs the requested operation and then does a modulo 24. Tomorrow, I could write a role for int (or is it an explicit role now? I've lost track of the syntax discussion) that does the same thing, but with fewer typecasting hiccups.

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 int in a two-field struct that contains both the hour value and the local number of hours per day in order for my + operator to have the context it needs. And yes, there might be some cases where you want a struct that carries the value around with its context, but if the context is already available, that's a waste of both memory and processor time (depending on how large the "context" you're carrying is).

Second example: what is equality? If I write a wrapper/role for string that represents names, I might want, in some cases, to compare them in their exact representation (to see if the user changed the field), or in a case-folded, normalized representation (to see if the user-supplied name is the "same name" as a known name), or in a Soundex-style representation (to see if the user input is "close enough" to a known name). All of these are types of equality, and all the usual traits of equality hold (there's a corresponding inequality, the operation is commutative, it's transitive, it's boolean-valued, etc). Only one of them can be represented with the == operator. Similarly, the relational operators aren't properly defined over the SIMD vector intrinsics because it's ambiguous whether the > operator should return true if all components are greater, true if any component is greater, or just a raw bitmask of which components are greater. So instead of v1 >= v2 we have Vector256.GreaterThanOrEqualAll(v1, v2), which puts things back into prefix notation (and, in this case, requires choosing the right implementation method for the type).

One final example: C# had to add an entirely new syntax to support user-defined checked operators, which are defined as independent methods, but there's no reason it had to have been implemented that way. Conceptually, "checked state" is a boolean-valued context which affects operator behavior. I wonder how many implementations of checked/unchecked operator methods simply delegate to and return the result of a three-parameter method with the parameter list (T left, T right, bool isChecked)? I can't imagine it's a small percentage.

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 checked, unchecked, and using:

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:

  • Syntax 1 also declares a local variable (on account of it looks like it does and let's not be confusing), and it is not allowed to shadow another name declared in the enclosing scope (if you want to forward a value, use syntax 2). It inserts an implicit named argument into the local scope with the given type, name, and value.
  • Syntax 2 doesn't use expression syntax. It's a comma-separated list of identifiers, each of which may have a typecast notation in front of it. This syntax does not declare new locals or assign new local values; all it does is insert the implicit named argument into the local scope using the name of the identifier (which must resolve to a value if used as an expression) for the name, and (a) the type of the identifier's value and the value of the identifier at the time of the implicit statement, or (b) the specified type and the result of typecasting the identifier's value to that type.
  • When implicit statements are nested, inner statements are allowed to shadow implicit named arguments declared by outer implicit scopes, and only the innermost declaration for that name takes effect.
  • When choosing overloads for a method invocation in a scope that has implicit named arguments, follow this process:
    1. Let the implicit argument match count be 0. Let the implicit argument match list be the empty set.
    2. Take the list of all possible overloads for this invocation, including extension methods. For each overload:
      1. Count the number of implicit named arguments declared in this scope that are valid for this particular invocation. For an implicit named argument to be valid, it must match the name of a parameter in this overload that has the implicit keyword specified, and that parameter must not be specified by any of the explicit arguments (positional or named) in this invocation, and the type of the implicit named argument must have an implicit (possibly identity) conversion to the parameter type.
      2. Add every valid implicit named argument to the invocation's argument list as named arguments.
      3. Add the name of every valid implicit named argument to the implicit argument match list, if it is not already there.
      4. If at this point, the overload is still missing required parameters, ignore this overload and move to the next.
      5. If the number of valid implicit named arguments in this invocation is less than the implicit argument match count, ignore this overload.
      6. If the number of valid implicit named arguments in this invocation is greater than the implicit argument match count, increase the implicit argument match count to that number and ignore all prior overloads in the list. (They had a lower match count.)
    3. At this point, the overload list has been filtered to the overloads with the greatest number of implicit arguments inserted (the implicit argument match count), and the implicit argument match list contains the list of variable names that were matched for this invocation. If the length of this list is not equal to the implicit argument match count, it means that there were valid implicit arguments declared in this context that did not get used. In this case, it is a syntax error for ambiguous overload resolution.
    4. Proceed with standard overload resolution, using the filtered overload list - pretend as though all ignored overloads were not declared.
  • This algorithm allows the developer to create a context in which their own extension methods are prioritized even over an instance method that exactly matches the invocation signature. This is intentional. If you declare implicit (auditLogger), it means that every invocation that can be logged, will be logged.
  • If any declared implicit named argument never gets used in an invocation during its scope, it is a syntax error, just like using a named argument that doesn't match a parameter.
  • It could go either way, but my gut feeling is that implicit named parameters do not create implicit named arguments in their scope. If you want to forward an implicit parameter as an implicit argument, then the alternate syntax in Hour.Add - which goes in the same position as a where suffix for a generic method - allows you to declare which implicit parameters (input) get used as implicit arguments (output), providing textual evidence of the dataflow even when using an IDE that doesn't provide an indicator for implicit parameter usage.
  • You cannot have an implicit out parameter, because there's no formulation of an implicit out argument that makes any sense. As for the other byref modifiers:
    • An implicit in parameter is allowed, and can accept an implicit argument specified by value, just like for explicit in parameters and their arguments.
    • An implicit (in varName) { ... } argument is allowed, and it applies readonly/in semantics to the local varName for the duration of the block, if it wasn't already.
    • I don't know whether implicit ref parameters and arguments should be allowed. My inclination is "yes", though, because implicit arguments are always written out explicitly somewhere near-ish to the callsite. You could use implicit (ref varName) to pass varName by ref to an implicit ref parameter, but you could also use it to pass varName to an implicit in parameter without forcing readonly semantics on the local variable. As a use case, you could implement the classic "running result status" pattern as an implicit ref argument:
      bool success = true;
      implicit (ref success)
      {
          var x = OperationOne();
          var y = OperationTwo(x);
          this.EndResult = OperationThree(x, y);
      }
      return success;
      Each of OperationOne, OperationTwo, and OperationThree could have success as an implicit [ref/in] bool parameter; those that can fail would have it as an implicit ref parameter, while those that only want to know if the operation has failed (to avoid performing side-effects, for example) would have it as an implicit in parameter. In order to do this today, you'd likely use bool-returning methods (meaning you can't use the return value as part of your logic as I did above) or use a try/catch (which potentially has much wider-reaching effects, as well as being a potential performance trap); this is simply a third option.

@dmchurch
Copy link

dmchurch commented Jun 5, 2024

@MadsTorgersen I think you're still the champion for this - do you or @radrow have any thoughts on my formulation above?

@radrow
Copy link
Author

radrow commented Jun 16, 2024

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 == operator. At the moment, in many cases it seems almost an anti-pattern to ever use == on strings instead of the formal String.Equals(String, String, StringComparison), for instance. Moreover, here we already have some sort of implicit parametrization in the form of Thread.CurrentThread.CurrentCulture.

One difference I see between steering == on strings and the base of modulo arithmetic for ints is that the latter belong to different algebraic structures. They are sort of different animals. By that, I enjoy restricting the freedom of conversions between modulo-ints of different bases.

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.

void SignUp(String name)
{
    implicit(StringComparison comparisonType = CurrentCultureIgnoreCase)
    {
        if(name == "Radek")
        {
            throw new WeDontLikeRadekException();
        }
    }

    implicit(StringComparison comparisonType = Ordinal)
    {
        for (other in database.names)
        {
            if(name == other) return;
        }
        database.insert(name)
    }
}

Regarding your mention of checked (which I see for the very first time), it looks like precisely an instance of implicit parametrization I am proposing here. The only difference I can immediately see (except being a hardcoded case) is that you can't declare a method to inherit it from the caller's context. The closest (but still far) you can do is to set it globally in the compiler. Additionally, I don't think operators should be considered special in any way here — why would x + x be allowed benefit from implicit contexts, while a custom .factorial() method would not? To me, the solution I propose makes it paradoxically more explicit than that, by enforcing declaration of the implicit context at the method's definition, which in case of checked is only mentioned in the language specification (if you have the right C# version, of course). Thank you for bringing this up, I didn't know about it.


I think the syntax you've shown is very clear. Since we have one-line using statements which span their scope to the end of the outer scope, I would also consider doing that with implicit too:

implicit int hoursPerDat = localPlanet.HoursPerDay;
return currentHour + hoursToWind;

Additionally, it may make sense to allow combining using with implicit, just like you can do await using:

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 where clause might be a better place for that.

I have no strong opinions on the ref and in parameters, but I see not reasons against. I find your example with success quite neat, resembling a bit the use of typical state transforming monads. I don't think there is much danger associated — in the end, you can always do your references by manually boxing any data.

@dmchurch
Copy link

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 int for the hour field.)

You're right that, given the one-line using syntax, we probably ought to allow a one-line implicit syntax; I'm not sure how I feel about using that syntax myself (I like having the visual indication of having the extra indentation), but I agree it's better not to violate expectations, and for all I know, once the feature lands I might start writing one-line implicit statements all over the place 😅

Especially since, yes, it makes perfect sense to combine implicit with using or await using! The use cases abound. I'd be inclined to put the implicit keyword outside the parentheses with the using keyword, preferably after: using implicit or await using implicit. I don't have any opinion on whether the reversed syntax implicit using should be a syntax error or just an alternate (perhaps discouraged) syntax.

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 using, the parentheses should not used for the single-line form. I'd suggest the following, inspired by the fact that a single var keyword is allowed to substitute for multiple types in its var (a, b, c) = (false, 1, "two"); deconstruction form:

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 var form of deconstruction declaration, though, since otherwise the following:

implicit (int hoursPerDay

is ambiguous. Is that the start of a multi-line declaration of implicit ints, or is it the start of a single-line implicit declaration for a deconstruction, where the first deconstructed element is an int? By requiring use of the var deconstruction declaration rather than the explicitly-typed deconstruction declaration, the syntax becomes unambiguous.

Of course, this syntax wouldn't be compatible with the using or await using keywords... unless the C# team decided to use this opportunity to allow deconstruction syntax with using 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests