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] Allow explicit declaration of anonymous types (in local collections) #3304

Closed
dsaf opened this issue Jun 4, 2015 · 39 comments
Closed

Comments

@dsaf
Copy link

dsaf commented Jun 4, 2015

C# thrives on being a multi-paradigm language. One of those paradigms is the imperative one, which is useful in some scenarios depending on personal preferences.

However it seems that there are some discrepencies between general feature availability in imperative and functional paradigms in C#.

One of such features is anonymous types. While it is easily possible to create a list of anonymous instances via functional paradigm (LINQ monad), it's not that easy to create same list in an imperative way.

Please find comparative examples below.

Functional:

private void FunctionalSample()
{
    var list = Enumerable
        .Range(0, 10)
        .Select(i => new {Id = i, Name = "Specimen #" + i})
        .ToList();

    // Further manipulations.
}

Imperative:

private void ImperativeSample()
{
    // Is there any other way?
    var list = (new[] {new {Id = -1, Name = "[ignored]"}}).Skip(1).ToList();

    for (var i = 0; i < 10; i++)
    {
        list.Add(new { Id = i, Name = "Specimen #" + i });
    }

    // Further manipulations.
}

This is obviously a pain point:

http://stackoverflow.com/questions/15749445/is-there-a-trick-in-creating-a-generic-list-of-anonymous-type
http://stackoverflow.com/questions/612689/a-generic-list-of-anonymous-class

The issue could be looked at in a bigger picture, e.g. class-level local inference of anonymous types (limited to private members, e.g. would allow returning an explicitly anonymous type (as opposed to object) from a type's private methods.

Suggested syntax could be something like:

private void NewSyntaxSample()
{
    // Using a suggested keyword, type inferred from first usage below.
    var list = new List<anonymous>();

    for (var i = 0; i < 10; i++)
    {
        list.Add(new { Id = i, Name = "Specimen #" + i });
    }

    // Further manipulations.
}
@HaloFour
Copy link

HaloFour commented Jun 4, 2015

Taking constructs added to the language in order to facilitate the functional aspects and forcing them into the imperative syntax doesn't make a lot of sense. There was an imperative solution to that problem long before anonymous types and type inference were added to the language. I think allowing for type inference to be driven by a potential eventual usage is opening an error-prone and confusing can of worms.

@dimaaan
Copy link

dimaaan commented Jun 4, 2015

Agree with @HaloFour
Moreover: if functional way is more suitable, why you dont want to use it?

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@HaloFour I don't insist on class-level inference, since it will be covered by language-level tuples - updated the post accordingly. Method-scoped anonymous type inference is already present though, it's just not consistent.

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@dimaaan I never said that. I use different approaches based on the problem at hand. Cramming everything into LINQ is not always a good idea.

@dimaaan
Copy link

dimaaan commented Jun 4, 2015

@dsaf look at following code

private void NewSyntaxSample()
{
    var r = new Random();
    // Using a suggested keyword, type inferred from first usage below.
    var list = new List<anonymous>();

    if(r.Next() % 2 == 0)
    {
        list.Add(new { Id = 1, Name = "Specimen #1" });
    }
    else {
        list.Add(new { Id2 = 1, Name2 = "Specimen #2" });
    }

    // How do i access list member??
}

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@dimaaan This is not how anonymous types generally work.

Consider the following example demonstrating your code rewritten using today's C#:

private void DoesntWorkAlready()
{
    var r = new Random();

    var list = Enumerable
        .Range(0, 10)
        .Select(i => new { Id = i, Name = "Specimen #" + i });

    if (r.Next()%2 == 0)
        list = list.Concat(new[] {new {Id = 100, Name = "AAA"}});
    else
        list = list.Concat(new[] { new { Id2 = 100, Name = "AAA" } });

    // The line above will give you an error ^.
}

@HaloFour
Copy link

HaloFour commented Jun 4, 2015

@dsaf

Method-scoped anonymous type inference is already present though, it's just not consistent.

Only based on the immediate usage. Given assignments are right-to-left the compiler already knows the exact type at the point of inference. This would require that the compiler not know the type until some indeterminate point.

Consider the following example demonstrating your code rewritten using today's C#:

Only because the compiler already knows the type based on the assignment to list. Without that the compiler can't determine the type of list until it hits one of the assignments. Clearly that would have to be a compiler error, but now it's far from the actual cause, and you cannot possible look at the declaration of list and know what it is upon instantiation.

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@HaloFour I am inclined to agree now, after some consideration. Can you think of any other alternative?

How about this:

var list = (new[] {new {Id = -1, Name = "[ignored]"}}).Skip(1).ToList();

vs

var list = new List<new {int Id, string Name}>(); // Anonymous, yet specified immediately.

It does look more and more like tuples though...

@dimaaan
Copy link

dimaaan commented Jun 4, 2015

var list = new List< new {int Id, string Name}>();

Looks better now

@HaloFour
Copy link

HaloFour commented Jun 4, 2015

@dimaaan That might work in Java due to type-erasure, but it suffers from the same exact problem at above in C# because the type of the generic type parameter isn't known and it must be known in order to complete the type and instantiate it. There is no System.Collections.Generic.List in the BCL.

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@HaloFour Well, a language-level list support is being considered, that might help, though it would push this change implementation into C# 8.

@mburbea
Copy link

mburbea commented Jun 4, 2015

Technically if you care about the potential allocations, you could write a helper

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static List<T> CreateListForAnonymous<T>(T dummy)
        {
            return new List<T>();
        }
...
// then later when you use it.
 var mylist = CreateListForAnonymous(false ? new {Id = 0, Name = ""} : null)

This is awful but at least then you're not instantiating the object. The compiler will simplify that method invocation as CreateListForAnonymous(null), and the jitter will inline it to the same as a direct call to new List<anonymous object>.

@HaloFour
Copy link

HaloFour commented Jun 4, 2015

@dsaf IIRC that's just C# syntax specifically related to creating System.Collections.Generic.List from literal expressions. The limitations/behaviors wouldn't change, just some of the syntax.

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@HaloFour true, but surely compiler can emit System.Collections.Generic.List<AnonymousClass123>?

Actually I think we are discussing different versions of @dimaaan's comment. I am not talking about new List()

@dsaf
Copy link
Author

dsaf commented Jun 4, 2015

@mburbea I am more worried about readability than performance, but yes that is a clever trick. It would be nice to avoid having to create new adhoc classes or using library-level tuples when writing local imperative code.

@HaloFour
Copy link

HaloFour commented Jun 4, 2015

@dsaf That it can, when it knows what AnonymousClass123 is. Which brings us back to the first problem. 😀

It amazes me how much unreadable mess developers are willing to write in order to avoid even the slightest boilerplate. A nested tuple/class is infinitely more readable. Messy tricks of inference should be avoided wherever possible. If the intention cannot immediately be understood it already costs more than being explicit. Any given piece of code will be read by a human significantly more often than it will be written, it is that first cost that should be minimized as much as possible.

@dimaaan
Copy link

dimaaan commented Jun 4, 2015

@dsaf @HaloFour Guys, thats my fault. First version of my last comment was new List(), because github eat some text.

But IMHO LINQ is more readable than var list = new List< new {int Id, string Name} >();.
That request may have some academic value, but useless in real world.

@dimaaan
Copy link

dimaaan commented Jun 4, 2015

I mean this feature increase complexity of language, but gives no profit

@gafter gafter changed the title [Area-Language Design] [Feature Request] Allow explicit declaration of anonymous types (in local collections) [Proposal] Allow explicit declaration of anonymous types (in local collections) Jun 4, 2015
@svick
Copy link
Contributor

svick commented Jun 4, 2015

@dsaf I view the proposed C# 7 tuples as pretty much a replacement for anonymous types (I doubt a language designed from scratch would have both). Is there any reason why you couldn't use a tuple instead of an anonymous type here?

You mention that tuples are "library-level". Is there anything specific about them that makes them unsuitable at a method level?

@mburbea
Copy link

mburbea commented Jun 4, 2015

if c#7 tuples are struct type, there are the usual pitfalls of structs to consider and thus they can have different performance considerations. Putting a struct of multiple object references, on the stack and copying it when pulling it out can have disastrous considerations, when a class would not have those issues.

@svick
Copy link
Contributor

svick commented Jun 4, 2015

@mburbea Yes, they are structs (see #347). I don't think what you're describing would be a common case. And for uncommon cases, I think it's okay to make you use a named type to achieve better performance.

@dsaf
Copy link
Author

dsaf commented Jun 5, 2015

@svick When I said "library-level" I meant today's Tuple<int, string>, when I said "language-level" I meant tomorrow's (int, string). Yes, I could use language-level tuples, but @mburbea is correct about performance and also there is a possibility that anonymous types will allow subtyping and interfacing thus becoming a more flexible and powerful option.

@gsscoder
Copy link

@dimaaan, I'm the first in favor of a functional style, but I'm agree with @dsaf about consistency (of two styles) and the need of being able to handle anonymous type with easy also when using imperative paradigm.

Sometimes right in favor of defining a method that adheres to functional paradigm from the outside, you may need more imperative flexibility from the inside (e.g. to handle mutation).

For example even a F# (multiparadigm but mainly functional) implements List.map using mutation to favor performance.

About syntax, even if I'd like to see new keywords only when absolutely necessary, I think that what suggested at the end of first post is readable and autoexplicative.

@mpawelski
Copy link

I think this "problem" can be solved by making type inference a bit "better". Compiler could see that you are creating a generic List object without specifying generic type and then see that you are using method of that object that consumes object of generic parameter type. It may sound complicated, but it's actually easy to understand. Actually F# has this feature already. Here is example that use named type, because F# doesn't have anonymous one:

open System.Collections.Generic

type NamedType = {
    Id : int;
    Name : string;
}

let testFunc() =
    let list = List<_>()    

    for i in 0..9 do
        list.Add({Id = i; Name = "Specimen #" + i.ToString()})

    list.Add({Id = 100; Name = "End"})

    //list.Add("just string");  //error if uncomment "This expression was expected to have type NamedType but here has type string

    list

let listOfNamedTypes = testFunc()

I am using wildcard character (_) when creating List object. Then the compiler see that you use Add(T item) and infer that T is of type NamedType. If you tried to do other Add call with different type then you'll have type inference compiler error.
C# could do the same, but probably with * character as wildcard (that was the proposition in pattern matching feature).

Personally I think it would be nice, because it's not only for using anonymous types but for also for other named types. So I could just avoid unnecessary typing that doesn't add much value to comprehend program logic and flow.

But I guess this feature doesn't add much value for given costs of development, so I don't have much hopes for this ;)

@dsaf
Copy link
Author

dsaf commented Jun 29, 2015

@mpawelski C# could definitely use a better type inference and lesser "ceremony" (although I am not a fan of F#'s total global inference) - a number of similar requests had already been raised, including #2319, #1470, #1419, #2397, #20.

@gsscoder
Copy link

@mpawelski,
this makes sense. C# actual type inference capability is too limited, maybe that it will never get F# type level type inference, but I think a lot can be enhanced.

@HaloFour
Copy link

"Better" is very relative. I'm personally not a fan of having type inference have to read through the remainder of the method to try to determine how the type will be used. It's more confusing to anyone who has to read and maintain that code.

@mpawelski
Copy link

@HaloFour Yes, it's very relative and subjective. Just like whole usage of implicit typing. Some people still don't like that we have something like var in C# (but I guess the number is declining ;)) but for many it's one of the best features introduced to C#. For me it's great that unnecessary type information don't bloat the code that you are reading and you can focus on understanding the flow and logic of code. In my experience when you have good naming of variables and methods then type information is unnecessary and just obscures the code. And if you want to know the type just hover the mouse over the var and IDE will tell you.
My proposition is just another "enhancement" to type inference that I think might be usefull and even improve readability of code.

When you say that

It's more confusing to anyone who has to read and maintain that code.

it reminds me that it's exactly what people were talking then var keyword was introduced to C#.

@HaloFour
Copy link

@mpawelski

It is all relative. I'm not opposed to some type inference. I like var (and have always liked var) and make frequent use of it. I'd like to see it expanded to delegates and fields as well. But I also like how simple the rules are.

Deferring the type inference based on usage complicates those rules and makes the developer have to keep additional things in mind when reading the code. It also introduces edge cases which would need solid rules, e.g.:

public class Baz { }

public class Foo<T1> {
    public void Bar<T2>(T2 value) where T2 : T1 { }
}

...

var foo = new Foo<*>();
foo.Bar(new Baz());  // legal?  If so, what is T1 or T2?

It's all a scale. The sweet spot is somewhere in the middle and different for everybody.

@dsaf
Copy link
Author

dsaf commented Jul 1, 2015

@HaloFour

foo.Bar(new Baz()); // legal? If so, what is T1 or T2?

  1. Legal, and this specific situation can be resolved by either using explicit syntax or defining rules of inference. Edge cases do not make the feature less useful for the majority of more common scenarios.
  2. T1 could be the "best common type" as mentioned in Stronger inference of a common type of a set of expressions #1419; if the inferred type is not as expected (e.g. the immediate parent type), user can specify expected type explicitly (e.g. parent type higher in the inheritance chain).
  3. T2 is Baz.

@dsaf
Copy link
Author

dsaf commented Jul 1, 2015

@HaloFour

I'm personally not a fan of having type inference have to read through the remainder of the method to try to determine how the type will be used. It's more confusing to anyone who has to read and maintain that code.

  1. Methods should not be so big that reading them becomes a problem.
  2. In 2015 it's an IDE problem really, it should show you the inferred type in a tooltip / badge.
  3. How do you feel about method-scoped types?

@gafter
Copy link
Member

gafter commented Nov 20, 2015

You probably want tuples (#347).

@dsaf
Copy link
Author

dsaf commented Nov 23, 2015

@gafter I guess, as long as fields/properties can definitely be named. Does it mean anonymous types will be "outdated"?

@paulomorgado
Copy link

They "can't". They still have usages.

Tuples won't have names outside of the source code. So, they won't be bindable. With enough work, the names could be put in expression trees for queriables, but does it worth the work, given that anonymous types already exist?

@alrz
Copy link
Member

alrz commented Nov 26, 2015

@paulomorgado for relevenat discussion see #6877. Since tuples are value types and going to be more common to be used with LINQ I think it does.

@paulomorgado
Copy link

@alrz, I'm aware of that discussion. But, while you think we can get rid of anonymous types, I think we can't.

@dsaf
Copy link
Author

dsaf commented Nov 26, 2015

If they get interfaces #13 they might become more interesting. But it feels like tuples will be preferable where possible. Of course you cannot get rid of anonymous types, they will just be rarely seen just like delegate, extern and goto.

@alrz
Copy link
Member

alrz commented Nov 26, 2015

@dsaf I think so. Unless you really want a reference type; assuming that expression trees support tuples, I can't think of any other use cases that you might prefer anonymous types over tuples.

@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.

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

10 participants