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: "Opaque" parameters #4629

Open
1 of 4 tasks
agocke opened this issue Apr 7, 2021 · 27 comments
Open
1 of 4 tasks

Proposal: "Opaque" parameters #4629

agocke opened this issue Apr 7, 2021 · 27 comments

Comments

@agocke
Copy link
Member

agocke commented Apr 7, 2021

Opaque parameters

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

Summary

This proposal would include a new syntax form for parameter declarations: an optional some
contextual keyword that precedes the parameter type, similar to ref or params. This would
look like:

void M(some IEnumerable<int> e) { ... }

The parameter type would be required to be an interface. The semantics would be that the
parameter type is not the interface itself, but an unspeakable generic type constrained to the
interface listed as the parameter type, e.g.

void M<T>(T e) where T : IEnumerable<int> { ... }

On the caller side, the call would look like List<int> x = ...; M(x). From the caller's perspective,
there would be no generic parameters to this method. The compiler would be required to infer
the appropriate generic parameter type, which should be the exact type of the argument.

Overloading:

some parameters should be valid as overloads, similar to ref params, and should be considered a better candidate than a parameter of the same type without some. This would allow for the following overload scenario:

void M(some IEnumerable<int> e) { ... }
void M(IEnumerable<int> e) { ... }

Users could then decide to add some overloads that would have improved performance and be preferred when present.

Motivation

This addresses a couple problems. First, it's very verbose to declare a signature of the above form,
although it's often very desirable for performance. Second, it hides irrelevant information (the generic
parameter name), which is not useful aside from the performance implications of the definition. Third,
due to the C# type inference algorithm, the verbose equivalent is often very difficult for the compiler
to infer. Because the parameter is not referenceable anywhere else in the parameter list, it should be
easy to infer the correct parameter type here.

Aside: credit to Swift for inspiration here, although the problems aren't the same.

Drawbacks

Extra syntax, more complexity.

Alternatives

TBD

Unresolved questions

Exact lowering and inference. A compiler-recognized attribute on the generated type parameters to remove
them from the list of user-visible type parameters may be necessary, along with a modopt on the some parameters
to enable overloading.

Design meetings

@YairHalberstadt
Copy link
Contributor

Am I right in thinking this only more performant if the type parameter is a struct? Is there any advantage to allowing you to constrain the type parameter to struct?

This sounds quite similar to impl trait in rust. Of course the killer feature of that is that it can be used for return types as well. Are there any thoughts about exploring that?

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 7, 2021

This feels very similar (morally and in design) to impl trait from rust (which is a good thing to me). :)

Dargh. Beaten.

@agocke
Copy link
Member Author

agocke commented Apr 7, 2021

This absolutely cannot be used in return types, since that would require existential types at the runtime level.

@YairHalberstadt
Copy link
Contributor

This absolutely cannot be used in return types, since that would require existential types at the runtime level.

Sounds like we now have an excuse to drive the runtime mad until they give use more type system goodies!

@333fred
Copy link
Member

333fred commented Apr 7, 2021

I'll champion this.

@agocke
Copy link
Member Author

agocke commented Apr 7, 2021

Sounds like we now have an excuse to drive the runtime mad until they give use more type system goodies!

That's partially me and I would be both excited and hesitant. 😄

@alrz
Copy link
Member

alrz commented Apr 7, 2021

For reference, this is the same as https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/flexible-types

Does this interact with "existential types" (#1328)?

@333fred
Copy link
Member

333fred commented Apr 7, 2021

Does this interact with "existential types" (#1328)?

Yep. This is a limited form of them, valid only in the parameter position. A more full implementation would be required for the return type inference that Yair and Cyrus mentioned above.

@JustNrik
Copy link

JustNrik commented Apr 7, 2021

what about

void Foo<T>(T foo)
    where T : not interface, IEnumerable<int>
{
    ...
}

@333fred
Copy link
Member

333fred commented Apr 7, 2021

what about

void Foo<T>(T foo)
    where T : not interface, IEnumerable<int>
{
    ...
}

That doesn't seem related. Nothing about this proposal says that the type substituted has to be a concrete implementation, it's purely about making signatures simpler.

@bernd5
Copy link
Contributor

bernd5 commented Apr 7, 2021

@JustNrik
As far as I understand the proposal: the idea is not to prohibit interfaces (those methods should be callable with an interface, too).

It is similar to: https://en.cppreference.com/w/cpp/language/function_template#Abbreviated_function_template
(even the syntax is nearly identical - just "some" instead of "auto" and a different order)

Perhaps we could use var instead of some here - which is the equilant to C++ auto.
In addititon we could make the Interface optional. This would allow for example code like:

int CombineHash(in var f1, in var f2)
{
   //just as simple sample
   return f1.GetHashCode() ^ f2.GetHashCode();
}

(this would be not as useful as in C++ - because it would still allow only the usage of object-member)

@agocke: nice proposal

@agocke
Copy link
Member Author

agocke commented Apr 7, 2021

@bernd5 That doesn't work. You can't use var since in my example you need to actually write out which interface you want it constrained to.

There's no type inference in my proposal except for normal generic type inference on the caller side.

@bernd5
Copy link
Contributor

bernd5 commented Apr 7, 2021

Why it should not work? It works even today:

using System;

class App {
    
    static int CombineHash<T1, T2>(in T1 f1, in T2 f2)
    {
       //just as simple sample
       return f1.GetHashCode() ^ f2.GetHashCode();
    }    
    public static void Main(){
        var a = 12;
        var b = 2L;
        
        var result = CombineHash(a, b);
        Console.WriteLine(result);
    }
}

The constrained interface would be simply the implicit "object-interface".

@agocke
Copy link
Member Author

agocke commented Apr 7, 2021

That's not what var means though -- it means infer the type based on usage. You're using it here to mean object.

What would you do when you actually need to write out the interface?

void M(var IEnumerable<int> e) { ... }

There's no place in the language where a type ever follows var. I don't see any good reason to reuse the keyword 'var' here.

@bernd5
Copy link
Contributor

bernd5 commented Apr 7, 2021

No, I don't mean object - I mean the actual type. object is only the root-type of all other types (just ref structs with limits - but they can't be used in generics anyway).
In my last example T1 is deduced to System.Int32 and T2 is deduced to System.Int64.

What you describe is actually what var does at every other place, too. With the interface you just limit the set of deducable types which could be used for overload resolution, too.

@agocke
Copy link
Member Author

agocke commented Apr 7, 2021

That description doesn't really tell me anything, it's just saying that var is another name for an unconstrained generic. How do you actually constrain it?

@quinmars
Copy link

This proposal would include a new syntax form for parameter declarations: an optional some
contextual keyword that precedes the parameter type, similar to ref or params

I think this should work more like an modifier, so that you can use it for nested types:

    void M(some IEnumerable<some IEnumerable<int>> t)
    {
        // ....
    }

would become:

    void M<T1, T2>(T1 t)
        where T1 : IEnumerable<T2>
        where T2 : IEnumerable<int>
    {
        // ....
    }

@Thaina
Copy link

Thaina commented Apr 19, 2021

This is too limited to make auto generic parameter. It can't infer concrete type (or even abstract type if I correctly understand this?) and cannot infer multiple interfaces. I don't think this worthwhile for making a new language feature for when we could just writing generic syntax normally (sure it is tedious but not that much)

Unless we already have #399

@agocke
Copy link
Member Author

agocke commented Apr 19, 2021

@Thaina

The problem is

void M<T1, T2>(T1 t)
   where T1 : IEnumerable<T2>
{
...
}

This is actually very desirable in a lot of random places but is completely uninferrable by C#. It's not the declaration side, which sucks but you write it once, that's the biggest problem. It's that the caller side has to manually specify the type parameters every time because there's no way to infer the substitutions.

@Thaina
Copy link

Thaina commented Apr 19, 2021

@agocke From that usage I think there was another issue that specifically improve generic system to allow that

Not so sure, maybe #1349 or #110 ?

@agocke
Copy link
Member Author

agocke commented Apr 19, 2021

I tried, couldn't make it work without terrible perf or a compat break. This is my backup solution. 😄

@Thaina
Copy link

Thaina commented Apr 20, 2021

Oh I see, then wish your best luck

@333fred 333fred added this to the Likely Never milestone May 20, 2021
@333fred
Copy link
Member

333fred commented May 20, 2021

LDM considered this on 5/19. We think that there's too many cliffs here: additional constraints, additional arguments of the same type, interactions with other type parameters, and the silent munging with the signature. We may consider something like this when we start looking more at traits and associated types, but this is rejected for now.

@Richiban
Copy link

I don't see anyone mention F#, which has what they call flexible types. It looks very similar indeed: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/flexible-types

@agocke
Copy link
Member Author

agocke commented May 21, 2021

@333fred Any discussion of type inference? This is kind of a blocker for certain high-perf scenarios and I don't have an alternate approach for fixing constrained type inference.

@333fred
Copy link
Member

333fred commented May 21, 2021

It was not discussed in any detail, no.

@RamType0
Copy link

The huge problem is that IEnumerable or IAsyncEnumerable is missing its enumerator opaque type.

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