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

(idea) Adding a 'static if' construct to support generic specialization #8871

Closed
jamesqo opened this issue Feb 18, 2016 · 15 comments
Closed

Comments

@jamesqo
Copy link
Contributor

jamesqo commented Feb 18, 2016

I was thinking about this a month ago or so when I read through the code of EqualityComparer. It had lots of code like this:

public static EqualityComparer<T> Default
{
    get
    {
        object comparer;

        if (typeof(T) == typeof(byte))
            comparer = new ByteEqualityComparer();
        else if (typeof(T) == typeof(short))
            comparer = new Int16EqualityComparer();
        ...

        return (EqualityComparer<T>)comparer;
    }
}

Obviously, while this is not a very common case, it's far from ideal. If we accidentally swapped byte and short for example, the library would still compile and we'd only get an exception at runtime.

Idea: static if

My idea is to have a static if construct (similarly to DLang), that the compiler could use to infer the type of T when specialized. With it, the code above would look like this:

public static EqualityComparer<T> Default
{
    get
    {
        static if (T == byte)
            // no casts needed, compiler knows that within this block T is a byte
            return new ByteEqualityComparer();
        else static if (T == short)
            return new Int16EqualityComparer();
        ...
    }
}

Of course, this wouldn't be the only use case for this statement. It could also improve performance in classes like List<T>, to specialize out Buffer.BlockCopy for the primitive types:

private static void OptimizedCopy<T>(T[] src, int index, T[] dest, int destIndex, int length)
{
    static if (T == byte || T == short || ...)
        Buffer.BlockCopy(src, index, dest, destIndex, length * sizeof(T)); // we can use sizeof(T) here since the compiler knows that T is primitive
    else // fallback to the general implementation
        Array.Copy(...);
}

Another case where this can be used is to supplant typeof(T).IsAssignableFrom, which is 1) not available in .NET Core, and 2) can't be optimized by the JIT. So code that looks like this:

if (typeof(T).IsAssignableFrom(typeof(U))) ...

could get transformed to this:

if (U : T) ...

which reads much more naturally. (The compiler could also do things like emit warnings if T is a struct, which it can't do with the runtime APIs.)

This could also be used to check if a type is a struct or a class, for instance:

class Activator
{
    T CreateInstance<T>()
    {
        static if (T : struct)
            return default(T); // all structs have default constructors
        else
            return ExpensiveCreateInstance<T>();
    }
}

Anyway, that's just my two cents. It should be clear that this is not a fully-formed proposal, it's just an idea I've been thinking about for a while in my head.

What do you guys think?

@Grauenwolf
Copy link

I don't know. I can see how it would be useful, but something seems wrong.

@pawchen
Copy link
Contributor

pawchen commented Feb 18, 2016

Maybe this need CLR support?

@Grauenwolf
Copy link

Probably not, as the compiler can just create an extra variable for each
if-block.

Jonathan Allen
Lead Editor, .NET Queue
InfoQ / C4Media
[email protected]
619-933-8527

On Thu, Feb 18, 2016 at 1:32 AM, Paul Chen [email protected] wrote:

Maybe this need CLR support?


Reply to this email directly or view it on GitHub
#8871 (comment).

@pawchen
Copy link
Contributor

pawchen commented Feb 18, 2016

What if a method using this feature is called from another generic method that doesn't use this feature?

@SirTony
Copy link

SirTony commented Feb 18, 2016

This seems like it'd be better as an extension to the where clause.

void Foo<T>( T x ) where T : sbyte, short, int, long
{
    // x is guaranteed to be a signed integer value
}

I'm all for lifting features from D and dropping them into C# wholesale - and there are plenty of features I'd like to borrow - but the addition of static if without full-blown templates or CTFE seems unnecessary when we've already got syntax for specializing generics to some degree.

@HaloFour
Copy link

A relevant CoreCLR issue: https://github.com/dotnet/coreclr/issues/2591

@alrz
Copy link
Contributor

alrz commented Feb 18, 2016

F# has it. But it's implemented on top of statically resolved generics. Also see this relevant blog post.

@alrz
Copy link
Contributor

alrz commented Feb 18, 2016

And this is a duplicate of #178 or #8194 (related: #3255, #4385)

@svick
Copy link
Contributor

svick commented Feb 18, 2016

we can use sizeof(T) here since the compiler knows that T is primitive

What IL would that compile to?

typeof(T).IsAssignableFrom, is not available in .NET Core

I believe you can use typeof(T).GetTypeInfo().IsAssignableFrom instead.

@Grauenwolf
Copy link

I like that a lot more. Besides just feeling better, it opens the door to useful things like support for arithmetic and comparison operations.

-----Original Message-----
From: "Tony J. Ellis" [email protected]
Sent: ‎2/‎18/‎2016 3:03 AM
To: "dotnet/roslyn" [email protected]
Cc: "Grauenwolf" [email protected]
Subject: Re: roslyn Adding a 'static if' construct to support genericspecialization (#8871)

This seems like it'd be better as an extension to the where clause.
void Foo( T x ) where T : sbyte, short, int, long
{
// x is guaranteed to be a signed integer value
}I'm all for lifting features from D and dropping them into C# wholesale - and there are plenty of features I'd like to borrow - but the addition of static if without full-blown templates or CTFE seems unnecessary when we've already got syntax for specializing generics to some degree.

Reply to this email directly or view it on GitHub.

@jamesqo
Copy link
Contributor Author

jamesqo commented Feb 18, 2016

@DiryBoy

What if a method using this feature is called from another generic method that doesn't use this feature?

How would that be a problem? As long as the method's initial constraints on T are a superset of the calling method's, I don't see how that could cause errors. e.g:

void A<T>() where T : class
{
    B<T>();
}

void B<T>()
{
    static if (T == Foo)
        ...
    else ...
}

@jamesqo
Copy link
Contributor Author

jamesqo commented Feb 18, 2016

@SirTony How would that help in specialization, though? As far as I can see, your method can only be called with a concrete type (e.g. Foo<int>) or a generic type with similar constraints.

@jamesqo
Copy link
Contributor Author

jamesqo commented Feb 18, 2016

@svick

What IL would that compile to?

A generic sizeof is already supported by the runtime: #3210 (comment)

@omariom
Copy link

omariom commented Feb 21, 2016

Actually typeof(T) == typeof(<a_value_type>) is already a JIT time constant.
For example, this method:

private int GetCodeForType<T>()
{
    if (typeof(T) == typeof(byte)) return 1;
    if (typeof(T) == typeof(DateTime)) return 2;
    return 3;
}

Will be JIT compiled to return 1; when T is byte, return 2; for DateTime and return 3; for other cases.

@maazl
Copy link

maazl commented Jul 22, 2016

Example of usage of partial specialization:

class Deduplicator
{
    public static string Deduplicate(string value)
    {   ...
    }
}

// strongly typed PropertyInfo to a property of type T in class E
class PropertyReference<E,T> : where E : class
{
    public readonly Func<E,T> Getter;
    public readonly Action<E,T> Setter;

    public PropertyRef(PropertyInfo pi)
    {   Debug.Assert(pi.PropertyType == typeof(T));
        Getter = pi.GetGetMethod(true).CreateDelegate<Func<E,T>>();
        Setter = pi.GetSetMethod(true).CreateDelegate<Action<E,T>>();
        if (typeof(T) == typeof(string))
        {   var setter = Setter;
            Setter = (E target, T value) => setter(target, Deduplicator.Deduplicate(value)); // error
        }
    }
}

The above code could for example be used to deserialize XML data content into deduplicated in memory data structures.
Of course the last line cannot compile because the restriction on the type T does not fit to the signature of Deduplicate.

To be closer to the current type constraints I would recommend a different syntax:

    where T : string
    {   var setter = Setter;
        Setter = (E target, T value) => setter(target, Deduplicator.Deduplicate(value)); // error
    }

This will in fact generate if (typeof(T) == typeof(string)) into IL and additionally tell the compiler that the related block has a narrowed type constraints.
May be in and out can additionally be used to specify allowed covariance. Unfortunately this is not consistent with other generic type constraints because they are always covariant.

@gafter
Copy link
Member

gafter commented Mar 24, 2017

We are now taking language feature discussion in other repositories:

Features that are under active design or development, or which are "championed" by someone on the language design team, have already been moved either as issues or as checked-in design documents. For example, the proposal in this repo "Proposal: Partial interface implementation a.k.a. Traits" (issue 16139 and a few other issues that request the same thing) are now tracked by the language team at issue 52 in https://github.com/dotnet/csharplang/issues, and there is a draft spec at https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md and further discussion at issue 288 in https://github.com/dotnet/csharplang/issues. Prototyping of the compiler portion of language features is still tracked here; see, for example, https://github.com/dotnet/roslyn/tree/features/DefaultInterfaceImplementation and issue 17952.

In order to facilitate that transition, we have started closing language design discussions from the roslyn repo with a note briefly explaining why. When we are aware of an existing discussion for the feature already in the new repo, we are adding a link to that. But we're not adding new issues to the new repos for existing discussions in this repo that the language design team does not currently envision taking on. Our intent is to eventually close the language design issues in the Roslyn repo and encourage discussion in one of the new repos instead.

Our intent is not to shut down discussion on language design - you can still continue discussion on the closed issues if you want - but rather we would like to encourage people to move discussion to where we are more likely to be paying attention (the new repo), or to abandon discussions that are no longer of interest to you.

If you happen to notice that one of the closed issues has a relevant issue in the new repo, and we have not added a link to the new issue, we would appreciate you providing a link from the old to the new discussion. That way people who are still interested in the discussion can start paying attention to the new issue.

Also, we'd welcome any ideas you might have on how we could better manage the transition. Comments and discussion about closing and/or moving issues should be directed to #18002. Comments and discussion about this issue can take place here or on an issue in the relevant repo.


Type classes would support this feature request, but with a completely different and typesafe syntax, and are under consideration at dotnet/csharplang#110

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