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: Statically resolved type parameters #8986

Closed
alrz opened this issue Feb 21, 2016 · 10 comments
Closed

Proposal: Statically resolved type parameters #8986

alrz opened this issue Feb 21, 2016 · 10 comments

Comments

@alrz
Copy link
Member

alrz commented Feb 21, 2016

Statically resolved type parameters

Considering that standard generic type parameters are mapped to CLR generics and have some limitations, statically resolved type parameters can be useful in conjunction with constraints that don't have a corresponding CLR representation, e.g. (static) member constraints, generic numeric code, etc.

Statically resolved type parameters are indicated by a caret symbol like ^T and can infer members and operator constraints. Although, constraint inference is only allowed for non-public methods because otherwise it's like we're inferring public contract from usage which will be hard to reason about.

static void F<^T>(T arg) {
  arg.F();
}

For more complex members, we can define interface-like types that only exist at compile-time (similar to TypeScript interfaces).

constraint Container {
  T Resolve<T>();
}

static object F<^T>(T arg) where T : Container {
  return arg.Resolve<object>(); 
}

Every distinct usage of the function F causes to generate a distinct overload with the type parameter ^T replaced with the actual type in use. And because of that, we can define rather complex constraints that are not dependant on CLR compatibilities and use them with arbitrary types. Note that constraint definitions cannot be public and IVT has no effect because they don't exist beyond assembly boundaries, and also they only shall be used as a constraint for statically resolved type parameters.

To safely expose the generated methods we can constraint them to specific types (#3255) — to be generated in the resultant assembly. For example,

public static T Sum<^T>(this IEnumerable<T> source) where T = int, double {
    T sum = default(T);
    foreach (T v in source) sum += v;
    return sum;
}

Generates two overloads for each of these types, namely int and double. The method body must be valid for each of those types in place. Constraint types are not allowed to be used in this context.

Using both kind of type parameters can become tricky but also can be useful to be able to use member constraints and take advantage of runtime generics as well. Statically resolved type parameters must be defined after any standard type parameter because they will be erased at compile-time.

static void F<T, ^U>(T foo, U bar) { ... }

Using statically resolved type parameters in classes probably needs generating distinct classes with different names which can result in unpleasant side effects; Type erasure (read Java generics) can be another option though but I'm not proposing anything in this regard.

PS: This is somehow related to #154 but it seems that it is just a list of nice-to-haves.

@benaadams
Copy link
Member

Also this could resolve and remove any typeof tests for the specific types as it does for value types

https://gist.github.com/benaadams/c6c109982f50c6f69af2#file-generictypeof-cs-L695-L738

@svick
Copy link
Contributor

svick commented Feb 21, 2016

How would this work across assembly boundaries (assuming no specific type constraints)?

Also, I think the syntax for constraints is confusing. For example, currently where T : class, IEnumerable means "it's a reference type and it implements IEnumerable", whereas where T : int, double would mean "it's an int or it's a double".

@alrz
Copy link
Member Author

alrz commented Feb 21, 2016

@svick

Every distinct usage of the function F causes to generate a distinct overload with the type parameter ^T replaced with the actual type in use.

I'm thinking that this shouldn't be allowed for public methods (with no specific type constraints) because in that case it's like we're inferring public contract from usage which will be hard to reason about.

PS: I've updated the opening post to address those ambiguities.

@orthoxerox
Copy link
Contributor

That sounds similar to my trait/typeclass proposal. However, I think that restricting compile-time generics to a single assembly greatly limits their usefulness. I am personally willing to wait until CLR can support them natively. Or perhaps something weird like an unusable class could be used to expose traits to external assemblies?

[Trait]
public abstract class TAddable
{
    public static TAddable Zero()
    {
        throw new InvalidOperationException();
    }

    public static TAddable Add(TAddable left, TAddable Right)
    {
        throw new InvalidOperationException();
    }

    private TAddable()
    {
        throw new InvalidOperationException();
    }
}

On second thought, this is a remarkably ugly declaration.

@alrz
Copy link
Member Author

alrz commented Feb 21, 2016

@orthoxerox Abstraction on top of primitives like what you suggest doesn't really fit into C# stack as a managed language. Rust or Haskell on the other hand, have a nice abstraction on top of primitives and you can define and expose your abstraction with zero overhead because a lot of things get inlined and everything. Personally I like the idea but I don't see it as useful in C#. For example take my example for Sum method. You can easily turn it into one or two methods without bothering yourself with any kind of abstraction on top of primitives, considering that they don't currently exist. In contrast, Rust's primitives and operators are implemented this way already. This is what F# has done afterall.

As for exposing traits, if interfaces could contain operators, static members, etc, there is no reason to introduce trait orconstraint types under this proposal.

@orthoxerox
Copy link
Contributor

You can easily turn it into one or two methods without bothering yourself with any kind of abstraction on top of primitives, considering that they don't currently exist.

@alrz Except that's not what you're doing. By explicitly enumerating all allowed types you are simply producing a better T4 template, since you cannot reuse your method for any other suitable type.

Yes, if interfaces could contain non-virtual members they would be as good as traits, if a bit more cumbersome to write. You could write (I replaced your ^ with a static modifier):

public interface IAddable<T>
    where T : IAddable<T>
{
    static T operator+(T left, T right);
    static T Zero { get; }
}

public T Sum<static T>(this IEnumerable<T> source)
    where T : IAddable<T>
{
    T sum = T.Zero;
    foreach (var item in source) {
        sum += item;
    }
    return sum;
}

@alrz
Copy link
Member Author

alrz commented Feb 22, 2016

@orthoxerox This proposal doesn't need CLR changes. Whereas, I have no idea how would your example work. Also, you IAddable definition is pretty limited. In Rust it is defined like

pub trait Add<RHS = Self> {
    type Output = Self;
    fn add(self, rhs: RHS) -> Self::Output;
}

Because there is no guarantee that rhs also be of type T and result in T but they have Self as default.

I'll note that F# falls back to reflection beyond assembly boundaries. Which, in my opinion, is horrible.

@orthoxerox
Copy link
Contributor

@alrz My proposal is yours. If no CLR changes can be done, interfaces with static members are destroyed when the assembly is built and can only be used to constrain statically resolved generic parameters.

If CLR is modified to support non-virtual interface members, then interfaces with static members are part of the API and can be used by any language that bothers to support them.

@alrz
Copy link
Member Author

alrz commented Feb 22, 2016

@orthoxerox Yeah, there are proposals for operators and static members in interfaces. I don't know if that is a reliable option anytime soon. But here I just assumed zero CLR changes. That's it.

@gafter
Copy link
Member

gafter commented Mar 20, 2017

We are now taking language feature discussion on https://github.com/dotnet/csharplang for C# specific issues, https://github.com/dotnet/vblang for VB-specific features, and https://github.com/dotnet/csharplang for features that affect both languages.

@gafter gafter closed this as completed Mar 20, 2017
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

6 participants