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: Collection, Anonymous and Function Types #7092

Closed
alrz opened this issue Nov 27, 2015 · 11 comments
Closed

Proposal: Collection, Anonymous and Function Types #7092

alrz opened this issue Nov 27, 2015 · 11 comments

Comments

@alrz
Copy link
Contributor

alrz commented Nov 27, 2015

Collection, Anonymous and Function Types

Following other proposals regarding introducing new kind of types, including slices (#120), tuples (#347) and intersection types (#4586), I want to propose language features for some other types mentioned in the following table.

Type Denote
Anonymous {T M1, U M2}
Dictionary (T: U)
List T{}
Function T(U)

Note that these are not the exact proposed syntax and they are meant to be altered based on feedback.

Anonymous Records

As it turns out, anonymous types caused a mass confusion. I see people try to use dynamic and InternalsVisibleToAttribute to access anonymous types' members within or across assemblies which is not a good idea IMO. I think intention of anonymous types is not obvious to people coming from other languages like JS and they just carelessly use them for various purposes which are not meant to be used for. Named tuples may address some of these use cases but still, since member names can be altered or erased (e.g. in patterns) they might be not a good option in some other contexts. The lack of ability to pass type-safe anonymous types around, sometimes cause to declaring trivial classes or even using reflection. To keep the best of both worlds, I propose anonymous records which are actually same as anonymous types in a sense, and they behave like tuples or records in the other.

anonymous-type:
{ anonymous-type-members }

anonymous-type-members:
anonymous-type-member-declaration
anonymous-type-members , anonymous-type-member-declaration

anonymous-type-member-declaration:
type identifier

From now on, anonymous-object-creation-expression returns a anonymous-type, for example:

var obj = new { X = 1 };
// equivalent to
{ int X } obj = new { X = 1 };

Class generation for these types remains the same for existing anonymous types, with the difference that if an assembly expose an anonymous record type, the generated class will be emitted as public. If some other assembly interact with these types, compiler would reuse the generated class and wouldn't generate a new one. For inter-assembly interactions, a new object would be created to be used for the target anonymous type. Since these classes are actually records, they can be used with the with expressions and can be deconstructed with a property-pattern without a type, for example,

var p = new { X = 1, Y = 1 };
// translates to
var p = new $AnonType(1,1);

p = p with { Y = 2 };

switch(p) {
  case { Y is 2 }: ...
}

No conversions between various types of anonymous records would be permitted, unless they possibly implement interfaces as proposed in #13.

Function Types

The behavior of existing delegates also raised some questions: why it's not possible to cast identical delegates to each other, i.e. Action<object, EventArg> is not the same as EventHandler, in despite of the fact that they are expected to be. From spec:

An interesting and useful property of a delegate is that it does not know or care about the class of the object that it references; all that matters is that the referenced method has the same parameters and return type as the delegate.

But still, they do care if you want to cast themselves to each other! It's like a sore spot that never seems to go away.

While Action<...> and Func<...> delegates can cover almost all use cases, still, they cannot be used with ref and out parameters, and you have to declare your own delegate types. Function types can address these issues, so you don't have to declare or use a specific delegate type just for these reasons.

Function types would translate to regular delegates but it is proposed that the following can be part of a function type for further compile-time validation and type safety:

Syntax

function-type:
empty-capture-listopt function-type-signature
type :: function-type-signature

empty-capture-list:
[ ]

function-type-signature:
return-type ( function-type-parameter-listopt )

return-type:
refopt type

function-type-parameter-list:
function-type-parameter
function-type-parameter-list , function-type-parameter

function-type-parameter:
function-type-parameter-modifieropt type

function-type-parameter-modifier:
ref
out

Examples

// local variable
bool(string, out int) parser = int.TryParse;

// accepts only a member of type Foo which returns T and has one parameter
void F<T>(Foo::T(Foo) f) {}

// equivalent to
void F<T>([MemberOf(typeof(Foo))] Func<Foo, T> f) {}

// note that Bar is a property and Foo::Bar returns an unbound accessor
F(Foo::Bar);

// accepts only functions that capture nothing
void G([]void() f) {}

// equivalent to
void G([NoClosure] Action f) {}

// usage
G([]() => WriteLine("Alright."));

List and Dictionary Types

To make #6949 more generally usable, I propose a language feature for list and dictionary initialization which also can be used with other types like JObject,

// infers a (int: int)
var map = { 1 : 2 };

// equivalent to
Dictionary<int, int> map = new Dictionary<int, int> { { 1, 2 } };

// we can make them invocable so one
// can use the type to create an empty list
var list = int{}(); /* or */ {int}();

// existing array initializer
int[] arr = { 1, 2, 3 };
var arr = new[] { 1, 2, 3 };

// infers a int{}
var list = [1, 2, 3];

// equivalent to
var list = new List<int> { 1, 2, 3 };

// infers a JObject
JObject json = { "foo" : "bar" };
var json = new JObject { "foo" : "bar" };

// equivalent to
var json = new JObject { ["foo"] = "bar" };

In the last example, nested maps will infer the type of the parent, in this case JObject. For lists, the target would have an Add with the second parameter of a non-abstract type that implements IEnumerable like JArray to infer.

This has some interactions with #2319 so if you want to create another class you should be able to omit type parameters, like

KeyedCollection<,> collection = { ... };
SortedList<> list = [ ... ];

If the context already provides type information, such as a function argument or an already typed variable, you can create an empty list or dictionary with [] or {:} respectively.

Syntax

dictionary-expression:
{ dict-elements ,opt }
{ : }

dictionary-elements:
dictionary-element
dictionary-elements , dictionary-element

dictionary-element:
expression : expression

list-expression:
[ list-elementsopt ]

list-elements:
expression-list , opt

@HaloFour
Copy link

@alrz

Seems to be too many things in one proposal here and many of them rely on other proposals that don't necessarily have a lot of traction.

I think trying to make anonymous types public and faking "equivalence" will end up being way more trouble that it's worth. It only takes two dependencies with similar "public" anonymous types and a need to pass from one to the other for the voodoo to break down in very non-intuitive ways. I'm still very much of the opinion that if you want a public-facing type that you should declare a public-facing type and not rely on some kind of magic to coalesce inline declarations into some form of public contract. I don't even like the prospect of it happening with tuples.

I don't see how the function type fixes the biggest problem with delegates and that being the lack of delegate type equivalence. Your syntax doesn't and can't fix the problem that EventHandler isn't Action<object, EventArgs> and one cannot be converted to the other without incurring the performance penalty that is a double delegate invocation.

I'm curious how the dictionary literal proposal will play out, but that probably belongs under #6949. List literals are probably a proposal unto themselves. The only real concern I have about them is that I don't think there would be a quick consensus as to what type the literal should be. C# devs would probably expect an array whereas JavaScript devs would probably expect a list.

I'm not sure exactly what fixed-size arrays really buy and this would be easily confused with actual fixed-size buffers (#126) which are a very different beast. At best they could be a normal array and an attribute. Rather than trying to nail a lot of very individual validations one-by-one I'd rather see a bigger story around argument validation and metadata to drive analyzers, #119 except with focus as to how the rules are encoded in the assembly metadata.

@alrz
Copy link
Contributor Author

alrz commented Nov 27, 2015

@HaloFour They all follow a common theme here, "new kind of types" so I decided to put them all in a single proposal. I don't mind opening new issues though.

I think, rather, the existing anonymous types are more voodooish, the compiler does all the work to make type-safe anonymous types and then hide it from developer. It seems that the only real use case for anonymous types is to facilitate LINQ operators. I just tried to make them more useful and lessen the voodoo going on behind the scene. Besides, for type equivalence I think there exist some more intuitive ways to solve it but I was not capable of thinking them up. Your objection regarding "relying on some kind of magic to coalesce inline declarations into some form of public contract even with tuples" is likely to be considered beyond this discussion. ;)

As mentioned before, double delegate invocation and delegate equivalence are not something that we can solve within the language, but having conversion implicitly handled by compiler is. Delegate declarations are way too ancient things that exist in the language, for a modern language, function type is an essential feature I believe. With method references, ref returns, capture lists, and all that jazz that is going to find its ways to the language I think function types can play a key role here.

#6949 is going in a different direction I think, when it has been said that it is going to use whatever type named JObject in scope, I realized that motivation behind that proposal is like "we need json". But motivation behind dictionary and list literals goes "we need a concise syntax for lists and dictionaries", "ok we can make it json-compatible". Speaking of consensus, I'd say it should be immutable collections. Arrays have already a type and a concise initialization syntax, the proposed feature is specialized for collection types. JavaScript arrays are mutable and you can add items to them. So a List<T> probably is the best option here.

Motivation behind fixed-size arrays as proposed here is that arrays which created by new definitely have a fixed size. You can think of them like null-safe variables, since most of variables are declared and assigned at the same time you can already guarantee that they wouldn't be null in any case. But when you return all that guarantee goes away.

@paulomorgado
Copy link

👍, @HaloFour .

@HaloFour
Copy link

@alrz

Anonymous types exist largely for projections, yes, and because of that they're not supposed to leak to the public. Any solution to try to make them public would be very messy. This is why tuples will rely on BCL provided structs. Given how simple it will be to define record classes I don't see a need for inline public anonymous type declarations.

Considering that the double delegate invocation thing comes up every time inferred delegate types come up I think that is the reason that there hasn't been an attempt to solve it in the language.

I took @gafter 's comment in #6949 to mean that the prototype was designed around JObject but that it wasn't necessarily what they were looking to add to the language. Using List for array/list literals is probably about right, but maybe it could also infer the type based on target.

@orthoxerox
Copy link
Contributor

What would be the type of a function that computes a cross product of two vectors represented as tuples, (double, double, double)((double, double, double), (double, double, double))?

@alrz
Copy link
Contributor Author

alrz commented Nov 29, 2015

@orthoxerox or Func<(double, double, double),(double, double, double),(double, double, double)>? what's your point?

@alrz
Copy link
Contributor Author

alrz commented Nov 29, 2015

@HaloFour I was just trying to address existing anonymous types' problems. I'll probably remove that part from suggestion, together with fixed-size arrays since as you pointed out #119 is a lot better solution.

@orthoxerox
Copy link
Contributor

@alrz my point was that type(s) => type was probably the better syntax.

@alrz
Copy link
Contributor Author

alrz commented Nov 29, 2015

@orthoxerox Maybe. Then T => U => V is Func<T, Func<U, V>> or Func<Func<T, U>, V>? Either way, it seems that C-style syntax doesn't play well with tuple and function types. I even prefer semicolon delimited tuple types so you could write List<T;U> instead of List<(T,U)>. It's just hurtful.

@orthoxerox
Copy link
Contributor

@alrz the former, as in all ML languages.

@alrz
Copy link
Contributor Author

alrz commented Dec 4, 2015

I have to give up function types too, because they also suffer from type equivalency issue just like anonymous types. As long as structural types/delegates are not supported, this is not possible.

As for collection types, I'd rather close this in favor of #6949. "we need to have one issue per issue."

@alrz alrz closed this as completed Dec 4, 2015
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

5 participants