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

Language support for KeyValuePair<TKey, TValue> struct and tuple mappings #11910

Closed
Opiumtm opened this issue Jun 10, 2016 · 36 comments
Closed

Comments

@Opiumtm
Copy link

Opiumtm commented Jun 10, 2016

KeyValuePair is very often used in C# code in many scenarios.
As there is (k, v) tuple syntax support, I suggest same support for KeyValuePair widely used struct.
#11530

As KeyValuePair and Tuple is a common pattern and sometimes there is custom Tuples or Keyed structs implemented, I suggest generalized built-in support for such tuple patterns and generalized support for keyed classes/structs at language level.

For such purposes I suggest new overloadable operators.

For tuple mappings (enable tuple-syntax initializing and tuple assignment)

public static (,) operator Class1(int field1, double field2)
{
    return new Class1() { Field1 = field1, Field2 = field2 }; 
}

and if class is generic:

public static (,) operator Class2<T1, T2>(T1 field1, T2 field2)
{
    return new Class2() { Field1 = field1, Field2 = field2 }; 
}

This will allow such things:

public Class1 SomeFunc()
{
    // assignment
    Class1 c = (2, 5.0);

    // return value
    return (1, 1.0);
}

So, KeyValuePair<TKey, TValue> should also support tuple operator and instead of new KeyValuePair<string, string>("key", "value") you can just write ("key", "value") if you're returning KeyValuePair from function, assign it to variable or use it as parameter for method.

And for object keys support.

// usage: extract key from object if it support keying.
var key = obj.!;

// define "key" operator
public static int operator .!(Class1 obj)
{
    return obj.Key;
}

// "keyed" constraint in generics

public interface IInterface1<T> where T: with key
{
}

Support for "value" operator

// usage: extract "value" from object
var value = obj.*;

// define "value" operator

public static string operator .*(Class1 obj)
{
    return obj.Value;
}

// if .* operator is not overloaded .* operator should return object itself.
// so default implementation for .* operator should be this, just returning object itself.

// default implementation of .* operator
public static Class2 operator .*(Class2 obj)
{
    return obj;
}

Extract types of keys and values at compile-time

Type keyType = typeof(Class1.!);  // get type of key
Type valueType = typeof(Class1.*);  // get type of value

Use types of keys and values in generics

public interface IKeyedCollection<T, T.!> : IDictionary<T.!, T> where T : with key
{
    void Add(T value);
    void Remove(T.! key);
}

public interface IKeyedDictionary<T, T.!, T.*> : IDictionary<T.!, T.*> where T : with key
{
    void Add(T value);
    void Remove(T.! key);
    T.* GetValue(T.! key);
}

T.! meaning type of key and T.* meaning type of value

And implicit (or explicit) operator conversion from keyed objects for existing APIs compatibility

// built-in implicit conversion for KeyValuePair from any keyed object
public static implicit<T> operator KeyValuePair<T.!, T.*>(T obj)
    where T: with key
{
    return new KeyValuePair<T.!, T*>(obj.!, obj.*);
}

Also added <T> for implicit<T> operator, so implicit conversions can accept generic source object argument

@Opiumtm Opiumtm changed the title Language support for KeyValuePair<TKey, TValue> struct Language support for KeyValuePair<TKey, TValue> struct and tuple-styled initializers Jun 10, 2016
@Opiumtm Opiumtm changed the title Language support for KeyValuePair<TKey, TValue> struct and tuple-styled initializers Language support for KeyValuePair<TKey, TValue> struct and tuple mappings Jun 10, 2016
@HaloFour
Copy link

Similar functionality is explicitly mentioned in the meeting notes here: #11205

However they are looking at much more general purpose syntax which works with existing types rather than defining new forms of operators and the like. Your proposal adds a lot of new language concepts for a rather narrow feature and I don't think it's worth it.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour this narrow feature is a starting point. I suggested general approach completely independent from Tuple and KeyValuePair types. "Tuple mapping" is a similar concept to tuple-like constructors, but it generalize approach and merge it with tuples as it allow not only construction but assignment of existing tuples to variable of any type which support tuple mappings.

As for keyed types it's not so "narrow" feature. Key/Value pattern is used massively and currently is tied to KeyValuePair struct. One obvious field of use for keyed types is a SQL-mapped ORM objects as any ORM entity have primary key and used with key in almost any data processing algorithms.

Operator overloading is a natural way to handle such features.

@HaloFour
Copy link

Why would you need new general purpose tuple operators when you could just use the existing implicit conversion operators?

public static implicit operator Class2<T1, T2>((T1 field1, T2 field2) tuple)
{
    return new Class2<T1, T2>() { Field1 = tuple.field1, Field2 = tuple.field2 }; 
}

The KVP-specific elements of this proposal involve adding some very non-C# syntax to C#. I'm not sure how you propose that those generic members would work given the nature of generics in the CLR.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour because (T1, T2) tuple in this case is a concrete type mapped to ValueTuple<T1, T2>
"Tuple mapping" operator is not an implicit conversion only, it's a generalized mapping from (value1, value2) syntax. If you're just assign "tuple" value to variable, you will have same functionality as "tuple constructor", if you're assign existing tuple object to variable it will work as implicit conversion. It's essentially a one-way mapping from tuple to named class.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour

The KVP-specific elements of this proposal involve adding some very non-C# syntax to C#. I'm not sure how you propose that those generic members would work given the nature of generics in the CLR.

I suggest approach similar to using with IDisposable pattern. So, for CLR level compatibility you should define IKeyValuePair<TKey, TValue> interface and implicitly implement this interface if you define keyed objects. On CLR-level with key constraint should translate to check for IKeyValuePair<TKey, TValue> interface.

@HaloFour
Copy link

HaloFour commented Jun 10, 2016

@Opiumtm

The mapping to an interface constraint isn't enough, you can't dot into a generic type parameter member to ascertain another generic type parameter. IKeyedDictionary<T> : IDictionary<T.!, T.*> is not possible with the CLR, the generic arity of IKeyedDictionary<> would have to be 3. The key and value types must be their own arguments.

public IKeyedDictionary<T, TK, TV> where T : IKeyValuePair<TK, TV> { }

It wouldn't be a good idea to automatically expand the arity as IKeyedDictionary<> and IKeyedDictionary<,,> are distinct types to the CLR and establish their public contract.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour to avoid implicit interface arity expansion it will be better to define IKeyedDictionary<T> as IKeyedDictionary<T, T.!, T.*>. So, interface will have same arity and you can explicitly set order of key and value type arguments in it.

Updated issue thread starting post.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour So, to sum up, on CLR level IKeyedDictionary<T, T.!, T.*> would translate to

public interface IKeyedDictionary<T, TKey, TValue> where T : IKeyValuePair<TKey, TValue>
{
}

If you do not define T.! or T.* type arguments it should be translated to object.

IKeyValuePair<TKey, TValue> should be defined as

public interface IKeyValuePair<out TKey, out TValue>
{
    TKey GetKey();
    TValue GetValue();
}

So you can assign IKeyValuePair<int, string> to unspecified IKeyValuePair<object, object> variable or method parameter.

So, IKeyedDictionary<T> where T : with key (you have omitted T.! and T.* arguments) will translate to

public interface IKeyedDictionary<T> where T : IKeyValuePair<object, object>
{
}

and if you implement other interface

public interface IKeyedDictionary<T> : IDictionary<T.!, T.*> where T : with key
{
}

would translate to

public interface IKeyedDictionary<T> : IDictionary<object, object> 
    where T : IKeyValuePair<object, object>
{
}

If you don't define T.! or T.* type arguments it mean you have no interest in its types and it would default to object and you can not use T.! or T.* types in your interface members, you can only use T type argument as you have not defined it in type argument list.

And another example:

public interface IKeyedCollection<T, T.!> : IDictionary<T.!, T> where T : with key
{
    void Add(T value);
    void Remove(T.! key);
}

will be translated to

public interface IKeyedCollection<T, TKey> : IDictionary<TKey, T> where T : IKeyValuePair<TKey, object>
{
    void Add(T value);
    void Remove(TKey key);
}

@HaloFour
Copy link

@Opiumtm So that would render those syntax changes unnecessary. You're not talking about a situation that occurs frequently enough to justify having to invent completely alien syntax just to save maybe a dozen keystrokes.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour I'm talking about very common and especially painful to use in real code pattern. Key/Value pairs are used in many places including modern Windows Runtime APIs. Keyed objects is used everywhere if object have unique ID.

It would add generalized syntax to such keyed objects and key/value pairs.

@jnm2
Copy link
Contributor

jnm2 commented Jun 10, 2016

Strongly dislike IKeyedDictionary. Every dictionary is by definition a keyed dictionary.
Also, other than reading .Key and .Value, I touch KeyValuePairs only three or four times in all code I've ever written. I agree, they can be a pain to write about, but that's so rare. If we're going to do something I'd much rather have a wider syntax that targets more types.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@jnm2 IKeyedDictionary is provided as practically useless syntax example, just to demonstrate IDictionary<T.!, T.*> interface inheritance and key/value support for generics as a whole.

@jnm2
Copy link
Contributor

jnm2 commented Jun 10, 2016

I think Key is superior to ! and Value is superior to *. It's clearer. It doesn't even save keystrokes; .k and .v reliably autocomplete.

@HaloFour
Copy link

I agree. C# is not a symbol-heavy language. Given the amount of backlash for suggesting use of ordinal members for tuple elements I can't imagine that there would be much support for special character identifiers for this one special case, especially special characters already mapped to operators.

I am warming to the idea of "tuple-like" conversion operators, but I do think that I would stick to the implicit operator syntax:

public static operator implicit KeyValuePair<TKey, TValue>(TKey key, TValue value) {
    return new KeyValuePair<TKey, TValue>(key, value);
}

// later

KeyValuePair<int, string> kvp = (123, "foo");
// which is really
KeyValuePair<int, string> kvp = KeyValuePair<int, string>.op_Implicit(123, "foo");

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour It's because you haven't worked on massive data processing.
Symbols are proposed for keys and values because Key, Value, Id or so on are concrete type members. When you rename concrete field/property on type, you should change many lines of code. Also, you should write too many boilerplate code for each concrete class (repeating over and over Id member reference) because you have no unified object key concept. Well, you can define your own IKeyedObject<TKey> interface, but it will be meaningful only for your code.

.! and .* syntax isn't reference to concrete type members. You can simply generalize your code to any type like this

public static IEnumerable<T> RemoveDuplicates<T, T.!>(this IEnumerable<T> source, IEqualityComparer<T.!> comparer = null)
    where T: with key
{
}

And so, you shouldn't reference to concrete Id member of concrete type.

If you haven't abstract "key" concept, you should write it as

public static IEnumerable<T> RemoveDuplicates<T, TKey>(this IEnumerable<T> source, Func<T, TKey> getKey, IEqualityComparer<TKey> comparer = null)
    where T: with key
{
}

You are forced to add Func<T, TKey> getKey parameter to get object key in a generailized fashion.

@Opiumtm Opiumtm closed this as completed Jun 10, 2016
@Opiumtm Opiumtm reopened this Jun 10, 2016
@HaloFour
Copy link

@Opiumtm

Thank you for questioning my credentials. That will certainly lead to constructive conversation on this subject.

Your argument is silly. Such small verbosity modifications don't warrant or justify completely rewriting the rules for member or type identifier naming. Manually writing keyed objects or keyed object containers is not a common task, for any style of programming. Typing ONE extra character doesn't slow you, the compiler, or the runtime down in any way that would affect processing data of any quantity. And, given Intellisense, you're not typing that character anyway. So, no, you shouldn't have to reference any concrete member or type member. C# should not become a read-only ASCII sneeze.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

I am warming to the idea of "tuple-like" conversion operators, but I do think that I would stick to the implicit operator syntax

This syntax isn't so clean.

public static operator implicit KeyValuePair<TKey, TValue>(TKey key, TValue value) {
    return new KeyValuePair<TKey, TValue>(key, value);
}

It's not clean because implicit operator is intended to be used with one and only argument. Also, it's implicit conversion for concrete type, not abstract tuple mapping.

@jnm2
Copy link
Contributor

jnm2 commented Jun 10, 2016

It's not clean because implicit operator is intended to be used with one and only argument.

In the same way that .Value is intended to be written .Value? Oh wait...

One interesting thing here, admittedly a bit off main topic, is that @Opiumtm wants to use TypeParam.MemberName as another type parameter. That's a radical new feature. I wonder if that could ever go somewhere useful.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour I reasonably question it because you're mentioned that object "key" issue is rare and "narrow". No personal offence. If you're saying it's rare and narrow - I can make assumptions on your typical application code profile 😄

Manually writing keyed objects or keyed object containers is not a common task, for any style of programming.

Well, it's quite common for a certain type of apps. That's what I meaning when I have "questioned" your typical app code profile.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour

C# should not become a read-only ASCII sneeze.

So what do you propose for abstract key and value concepts?
Keep in mind that you should not tie to concrete type members and try to substitute it with some abstract identifier.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@jnm2 I propose to not use type member name at all.
If you need to generalize it, you should not reference concrete type member.
I suggest .! and .* overloadable operators for this purpose with implicitly implemented interface for reflection and CLR level compatibility.

@HaloFour
Copy link

@Opiumtm

I propose not doing anything. Adding such radical new syntax simply so that you can reinvent KeyValuePair<TKey, TValue> repeatedly has limited-to-no value-added potential. If you don't want to refer to members as members by their given names I suggest using one of the various other languages that look like the result of cats walking over keyboards.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

reinvent KeyValuePair<TKey, TValue>

Not reinvent, but avoid it. Well, so verbate code looks much readable if it's rigged by many KeyValuePair<string, string>? Not think so. It looks like System.String for each string.

@svick
Copy link
Contributor

svick commented Jun 10, 2016

@Opiumtm I don't understand. How is the .! operator any more general than a Key property or GetKey() method?

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@svick it's more general because it's not a type member and have no name. When you type .! you tell compiler to get key from object and it will substitute .! with defined .! operator call. It's not so different from implementing some arbitrary interface, but it's not tied to any type or member on language level. So, your API will be not tied to concrete types or members. Library author can just require with key as type constraint and use T.! to extract key. It's why abstract concepts are used. To make things implementation independent. Generics was added for similar purposes.

@HaloFour
Copy link

That doesn't make it more general, it just makes it less readable. This proposal doesn't add anything new to the language, it just replaces proper member names with characters.

If you want to avoid typing out KeyValuePair<,> I suggest using aliases, or turning Intellisense back on.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@HaloFour I think verbose syntax often make things less readable. Visual Basic code is definitely less readable than C# code because of its verbosity. And this .! operator isn't completely for KeyValuePair. It's mostly for generic algorithms to avoid unnecessary code to extract keys and values, as it quite common task in generic collection processing algorithms.
Similarly, tuples (a,b) can be written as classic new Tuple<T1,T2>() but it's not readable and overtly verbose.

@svick
Copy link
Contributor

svick commented Jun 10, 2016

@Opiumtm

it's more general because it's not a type member

Operators are still type members.

When you type .! you tell compiler to get key from object and it will substitute .! with defined .! operator call. It's not so different from implementing some arbitrary interface, but it's not tied to any type or member on language level.

I don't see the difference:

obj.Key compiles to:

ldloc obj
callvirt instance int32 C::get_Key()

obj.! would compile to:

ldloc obj
call int32 C::op_Key(class C) // or whatever would be the name of .! in metadata

Neither of the two is more general.

@Opiumtm
Copy link
Author

Opiumtm commented Jun 10, 2016

@svick operators are type members only on CLR-level, but not on language level.
On language level it's an anonymous generic key.

Same goals can be achieved by defining a generic interface with GetKey() member (so, generic algorithm would extract key by call to GetKey() interface member), but there will be much confusion across independently created libraries which would reinvent such interface for its own use.

In particular, ORM libraries does this and each time reinvent their own means to extract entity keys in generic fashion. Entity Framework, for example, have an entity context method to get key from arbitrary entity.

@HaloFour
Copy link

HaloFour commented Jun 10, 2016

An unary operator that returns the component result of a calculation on that type is a member as much as any other. However, operators are static, which also means that you're eliminating the possibility of them being virtual. That immediately removes interfaces from the equation, both for reusability and for use as a generic type constraint.

Also, .! isn't an operator. That would conflict with the existing member operator ., which leaves ! as a member by definition.

@svick
Copy link
Contributor

svick commented Jun 10, 2016

@Opiumtm The MSDN C# Programming Guide disagrees with you, it lists operators among type members.

And so does the C# specification (from section Class members):

A class declaration may contain declarations of constants, fields, methods, properties, events, indexers, operators, instance constructors, destructors, static constructors and types.


but there will be much confusion across independently created libraries which would reinvent such interface for its own use.

That sounds like you may want to propose including the IKeyValuePair<K, V> in the base class library.

@DerpMcDerp
Copy link

What's the appeal of having 2-tuples convert to/from KVs rather than having a dedicated syntax for them? e.g. something like k -> v denotes new KeyValuePair(k, v) and k :> v denotes new KeyValuePair("k", v)

@jnm2
Copy link
Contributor

jnm2 commented Jun 11, 2016

C# does what it does quite well. The right way to generalize keys and values is at the class library level with an interface, not at the language level. C#'s focus is not primarily Entity Framework or any other keyed data.

Same goals can be achieved by defining a generic interface with GetKey() member (so, generic algorithm would extract key by call to GetKey() interface member), but there will be much confusion across independently created libraries which would reinvent such interface for its own use.

But that is exactly how such an interface would prove the worth of the concept in the first place. Failing that, you definitely do not want it as a language feature.

An interface in a library provides semantic context; what does GetKey() mean for this library vs another library or my program? There is no universal demand for "I know nothing else about this object, but get me a key for it." The closest thing I can think of is view data binding, but that is already handled satisfactorily.

@alrz
Copy link
Contributor

alrz commented Jun 11, 2016

Language support for KeyValuePair<TKey, TValue> struct and tuple mappings

I assume KeyValuePair<TKey, TValue> keyValue = new ( ... , ... ); works. See #11205.

@aluanhaddad
Copy link

@Opiumtm This sounds like a form of structural typing involving nominal (! and * are names) static members that can be used in constraints.

If so, there is no gain from making these static, as that precludes constraints, and the member names are irrelevant, so specifying additional non standard symbolic names is not helpful.

Basically, this seems like a proposal for structural typing with KeyValuePair<TKey, TValue> as a motivating usecase.

Some form of typesafe structural typing would be very powerful, so I am sympathetic to the idea but why not make it truly abstract by forgetting KeyValuePair<TKey, TValue> and the ! and * symbols and creating a structural typing proposal?

@jcouv jcouv self-assigned this Aug 9, 2016
@jcouv jcouv added this to the 2.0 (Preview 5) milestone Aug 9, 2016
@jcouv jcouv modified the milestones: 2.0 (RC), 2.0 (Preview 5) Sep 7, 2016
@VSadov VSadov modified the milestones: 2.0 (RTM), 2.0 (RC) Oct 6, 2016
@jcouv jcouv modified the milestones: 2.1, 2.0 (RTM) Jan 11, 2017
@jcouv jcouv modified the milestones: Unknown, 15.1 Apr 3, 2017
@jcouv jcouv removed their assignment Apr 3, 2017
@CyrusNajmabadi
Copy link
Member

We intend to support key/value pair support in teh collection literals feature. Thanks!

@CyrusNajmabadi CyrusNajmabadi closed this as not planned Won't fix, can't repro, duplicate, stale Nov 8, 2022
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