[go: nahoru, domu]

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]: Expand the use of 'not' as a generalized inversion operator in C# #5976

Closed
1 of 4 tasks
CyrusNajmabadi opened this issue Apr 1, 2022 · 19 comments
Closed
1 of 4 tasks

Comments

@CyrusNajmabadi
Copy link
Member
CyrusNajmabadi commented Apr 1, 2022

Expand the use of 'not' as a generalized inversion operator in C#

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

Summary

Expand 'not' operator to be viable in sensible places in the language beyond just patterns.

Note: not doesn't have to be the operator we pick. If we feel like ! is more suitable we could go with that. However, based on the reaction to !! i am somewhat wary about overloading the meaning of ! anymore. So I think using not will be the most palatable and will read particularly well.

Motivation

A programming language in general (and C# in specific) often has the need to relate entities in their domain to sets (for example, the set of booleans, the set of types, the set of contraints etc.). In these cases the language provides very suitable constructs for testing set containment. However, while less common, it is also just as reasonable to want to relate entities to the inverted form of that set. After all, such an inverted form is still just as reasonable a set on its own as the other set, so supporting that case would make certain problem spaces more pleasant. In some cases in the language today it is trivial to do this manually. Small finite sets, for example, can be inverted just by listing out all the members of the inversion of the set manually.

Programming languages recognize the common need for this with expressions. For example, the need to test if something is not in the set 0 or 1, leading to trivial operators like !(x == 0 || x == 1). However, where this gets painful though is either when the inversion is extremely large. It also becomes impossible if the inversion is infinite as there is no way to enumerate all the case cases in the language constructs today. Patterns need this routinely, which led to the addition of the not pattern a generalized mechanism to do set/matching inversion. I propose expanding on the use of this operator to all set-based contexts in our language today in a clean, clear and composable fashion.

Realistically, we will likely not be able to get such support in one version for all the cases where it is possible. However, we should consider them all and front load the ones that make the most sense, while also thinking about where we might want to expand on this in the future. We can drive particular cases now and in teh future based on community feedback and how important/applicable the use cases are to partners and the ecosystem as a whole.

In practice, my belief is that the type space is the most fruitful one to start with for set inversion. We technically already support type-inversion today in patterns through things like if (x is not Y). However, types are replete in our language in other locations, and I believe those other locations would benefit greatly from having this construct. The first location for this would be in parameters. For example:

public void Process(object value) => ...;
public void Process(not object value) { ... }

Logically, we want one method to be the catch all that is used when the caller truly has no idea what they have (for example, if they're using reflection/dynamic/other-slow-things). However, if they do have a strong type, we want to follow another path where we can assert that if htey were working with a strong type that it must follow certain sensible rules that indicate they know what they're doing. For example, in XLinq this occurs as the set of non-object supported types is fixed, but annoying to always spell out. While union types could solve this, the user would have to spell out often a large set of types, which would be extremely unpalatable). However, this does raise an important question about how not is to be interpreted. There are two viable perspectives, which are complimentary, but subtly different. The first is that not gives you the inverted set (e.g. everything other than what was specified). The second is that not disallows a particular entity (or set of entities). These may appear the same, but can mean very different things. As an example, consider the following:

using not Newtonsoft.Json.Linq;
using System.Text.Json;

For this to be sensible at all, the first line likely cannot mean "Pull in every namespace that is not Newtonsoft.Json.Linq". However, we should consider supporting just that even though it might seems initially absurd (and likely impossible to even pull off). Perhaps there are some problem spaces where it makes sense to be pull in everything (especially where people own the entire domain under their control). For example, one might do import not System; to get every symbol they own, and nothing from the BCL. However, even if we don't go that route it might be reasonable to imagine projects that want to prevent accidental usage of a particular namespace with similar concepts in certain parts of the codebase. Here, for example, we want to ensure that only the platform Json processing components, while not calling out into a different library (which may have subtly different semantics). While analyzers could suffice for this purpose, i believe there has been enough feedback from customers asking for a lightweight way to disallow usage of particular Apis to warrant a very simple construct to enable that like the above.

Of course, as we consider this space further, the possibilities for how we can use this operator become more and more interesting and exciting. In the BCL, Roslyn, and Language space, we've constantly run into issues into how to prevent certain APIs from being used (e.g. referenced in source), while also ensuring that programs do not break wrt to their ABI. This ensures that as people recompile they move off of these 'banned' APIs, while ensuring consumers that cannot recompile continue to work. In the past we've attempted to accomplish this with varying levels of success (honestly, total failure) through features like EditorBrowsable, Obsolete(error: true) and modreqs. The problem with this is that all of these required some amount of cooperation between components and languages in the ecosystem. We have cases where all of the above are just flat out ignored (including in varying versions of Roslyn compiler itself) leading to painful migration and transition stories for components. In line with recent discussions around the file scope, we would now allow not to be used as a scope like so:

class Component
{
    not public void ExistingMethod() { ... }
}

Obviously, this would only be viable if it solves the needs of both needing to preserve ABI compat while also breaking all source recompiles. To that end, this makes use of a extension of type-forwarders that the runtime is working on called member-forwarders. The implementation strategy here for Roslyn would be that the above would now be generated with a completely mangled name in an entirely different namespace/type. All these namespaces/types/member-names would be entirely mangled (similar to what we do today when we emit <PrivateImplementationDetails>). We will also emit a MemberForwardedFrom attribute (similar to TypeForwardedFrom) to inform the runtime where this member was originally. At dll-load-time/member-resolution-time, the jit/runtime would synthesize the appropriate method on the type ensuring that binary compat was maintained. This would be an airtight way to ensure source would have to be updated as the only way any other tool could possibly use this was if their compiler actively sought out to subvert these mechanisms (as opposed to things like modreqs where compilers can easily accidentally forget to check, and end up thinking something is allowed when it is not).

Lastly, the final place i think we could find the most value in a not feature and should consider front loading into an upcoming release is a new pattern around designing APIs with common 'inverted' operations. Just as operators often come in pairs (+ vs -, * vs /, checked vs unchecked), there are often lots of APIs that have pairs of useful operations that are compliments of each other. For example: Read and Write are virtual inverses of each other. Similarly, Parse and Format. Serialize/Deserialize. Save/Load. Attach/Detach, Expand/Collapse, Insert/Delete, Enable/Disable, Start/Stop, Acquire/Release, Cos/Sin, Math.Pi/Math.E. etc. etc. etc. These patterns are so common that we do not give a second thought to the common redundancy in naming and code organization here. To that end, i propose an extremely powerful new mechanism both for defining these pairs of constructs, as well as then using them from code:

class Component
{
    public void Attach(string information)
    {
        // perform all attach logic here.
    }
    not
    {
        // perform all detach logic here.  access to the variables in the upper block is allowed.
    }

Similar to the auto-prop field proposal, this enables the pairing of similar functionality (like with property accessors) while also giving those paired members the ability to share state that other members of the type cannot see. For example, the above code may have a bool attached local they can use and assert on, without having to worry about other members of the type examining (or worse yet mutating) that field. This is effectively the property-pattern expanded to methods, but not restricted to simple value get/set. Note: if the signatures of the inverse operation need to be different that will be possible through things like not (string information, int otherData). Calling these members is of course necessary. With properties, it is clear contextually if a read, write or both is happening. With normal methods though, that cannot be determined syntactically. To that end, in line with the declaration syntax, the invocation syntax can be written as:

void Dispose()
{
    _myComponent.not Attach("info");
}

! While this might look a little strange at first, it's reall no different from using not or ! for any other sort of negation to pick another choice. Users will soon pick this up and understand this well, and i think the virtue of not having to come up and use antonymous names for these pairs will be embraced wholeheartedly. Users hate repetition (the source of the DRY principle). So now instead of needing to have the repetition in things like Serialize and De*serialize* (which also have odd inconsistencies in casing), you now simply have Serialize and not Serialize. All pairs of inverse concepts now can always have a single name with a single consistent pattern in terms of how to define them together and easily call them. Indeed, if we imagine common compose/dispose sequences, the inverse sequence is simply the not form of of the compose sequence in reverse. To that we end, we might even consider one final form to tie this all together:

public MyComponent()
{
        _myComponent1.Acquire("...");
        _myComponent2.Start("...");
        _myComponent3.Attach("...");
}

void Dispose()
{
    not
    {
        _myComponent3.Attach("...");
        _myComponent2.Start("...");
        _myComponent1.Acquire("...");
    }
}

Alternatives

The above cases just scratch the surface of where I think not will be viable in the language. In the interest of targeting use cases that the community finds most useful though, we should seek out as many additional cases as possible. So, if you are reading this and can think of places you'd like us to broaden the language with, please list them below. All ideas are welcome as i think this will be a big boon to c# and will certainly not (har har) be a detriment to the language. Thanks!

@CyrusNajmabadi
Copy link
Member Author
CyrusNajmabadi commented Apr 1, 2022

Assigning to @333fred who expressed the most interest in seeing this happen. I believe we have the time in the current release to get started on this and have prototypes out soon for consumption. If necessary, work on current features should likely be postponed to front load these options instead.

If we cannot get these in now though, i suggest waiting a year and restarting work on them at that point.

@333fred
Copy link
Member
333fred commented Apr 1, 2022

I don't make scheduling decisions, that's an @jaredpar question. I just write the notes.

@333fred 333fred assigned jaredpar and unassigned 333fred Apr 1, 2022
@CyrusNajmabadi
Copy link
Member Author
CyrusNajmabadi commented Apr 1, 2022

Adding @jcouv who has expressed feelings of dissatisfaction with the current working set, and who has been itching to write more proposals for features that will be more beneficial to the language. Also, a few more j aliased interested parties.

@jnm2
Copy link
Contributor
jnm2 commented Apr 1, 2022

We might need a followup imaginary operator. not 3 is -3 but imaginary 3 lets you be more expressive in complex domains. Also, running imaginary code is cheap too, nice perf boost.

@CyrusNajmabadi
Copy link
Member Author

Also, running imaginary code is cheap too, nice perf boost.

It depends on the representation. I think we could have an imaginary64, which is built out of two floats (for the real and imaginary components). This would allow modern 64bit architectures to efficiently execute these instructions. @tannergooding to weigh in on the viability of this (as well as the above proposal and how it might help numerics). Given that numerics are entirely built out of pairs of complimentary operations (like Min/Max, Ceiling/Floor, Log/Exp), i think that numerics would be one of the first APIs to adopt the not pattern. We should strongly considering holding back the release of the numerics work in .net 7 (and perhaps .net 7 itself) until we can bring this online.

@HaloFour
Copy link
Contributor
HaloFour commented Apr 1, 2022

I dub thee the "Borat" operator and look forward to seeing it haphazardly rushed into C# 11.

I also think that this should be a postfix operator:

if (this.Suit is Black not) ;

@PathogenDavid
Copy link

Is there an update on the status of this feature? It's been over 20 minutes since it was posted and ages since anyone from LDM has even acknowledged it. Other languages have had this for years now. Does Microsoft even care about C# anymore????

@CyrusNajmabadi
Copy link
Member Author

Is there an update on the status of this feature?

I am escalating this up the chain of command as fast as i can. It's a friday, so most people are not even working. Will try to post updates as i learn more.

@333fred
Copy link
Member
333fred commented Apr 1, 2022

It's a friday, so most people are not even working.

Can confirm, I'm going to be setting up an FRC event this afternoon.

@CyrusNajmabadi
Copy link
Member Author

Linking to relevant TypeScript proposal which we may want to consider alongside this.

Though there is a misspelling in their title. It should be "not-fungible types", not "non-fungible types".

@DanielRosenwasser
Copy link
DanielRosenwasser commented Apr 1, 2022

I really like the direction with this proposal. I think my only concern is that you need to keep the syntax space composable. For example, you listed a few examples between dual operators like + and -. You also showed an example of !. In some ways, not is closer to ! in that there's no dual operator - its dual is simply the absence or double-application of the operator.

Dual Operators

If you wanted to move closer to + and -, you need to add another operator. If you don't want to add a new contextual keyword, consider continue or is? I don't have anything better here to be honest, but those might suffice.

So using your examples above, the following

void Yadda()
{
    continue
    {
        _myComponent3.Attach("...");
        _myComponent2.Start("...");
        _myComponent1.Acquire("...");
    }
}

would desugar to

public Yadda()
{
    _myComponent1.Acquire("...");
    _myComponent2.Start("...");
    _myComponent3.Attach("...");
}

Stacking Operators

Alternatively, if you want to be consistent with !, you need to allow not to stack appropriately. I'm not sure if the language supports this today. Using your examples above, the following

void Yadda()
{
    not not
    {
        _myComponent3.Attach("...");
        _myComponent2.Start("...");
        _myComponent1.Acquire("...");
    }
}

would desugar to

public Yadda()
{
    _myComponent1.Acquire("...");
    _myComponent2.Start("...");
    _myComponent3.Attach("...");
}

@jasonmalinowski
Copy link
Member

I'm not so sure this is a good idea, but I'm not not for thinking about it more. I'm sure the set of examples that @CyrusNajmabadi gave is not not not exhaustive, and I'm not not not not sure the community will think of other even more motivating examples and I can't wait to see those. I'm liking the syntax here, since it's not not not not not like other recent proposals we've had where using ! too many times can get confusing. It also does not not not not not not allow repeated use of a token over and over again, which @jaredpar always appreciates.

@CyrusNajmabadi
Copy link
Member Author

Hrmm. I hadn't considered that @jasonmalinowski. That definitely makes me wary as my brain is pretty terrible at understanding double negatives

@alrz
Copy link
Contributor
alrz commented Apr 1, 2022

Glad to see first-class support for not is coming to C#.

@JoeRobich
Copy link
Member

I am not looking forward to implementing this syntax in the C# textmate grammar.

@DanielRosenwasser
Copy link

I am not looking forward to implementing this syntax in the C# textmate grammar.

Kind of a given, nobody likes implementing any syntax in textmate grammars.

@jmarolf
Copy link
jmarolf commented Apr 1, 2022

Thanks for assigning me to have a look @CyrusNajmabadi. While I think you've covered the statement case well have we considered that we may want the ability to not any expression in the future?

@DanielRosenwasser
Copy link

That definitely makes me wary as my brain is pretty terrible at understanding double negatives

Can Roslyn provide elision regions in the editor to combine not (not not)+ into just not?

@333fred
Copy link
Member
333fred commented Apr 2, 2022

April fools is over. Closing out.

@333fred 333fred closed this as completed Apr 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests