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: Nested local type declarations #9523

Closed
jnm2 opened this issue Mar 7, 2016 · 34 comments
Closed

Proposal: Nested local type declarations #9523

jnm2 opened this issue Mar 7, 2016 · 34 comments

Comments

@jnm2
Copy link
Contributor

jnm2 commented Mar 7, 2016

Extend the languages to support the declaration of types in block scope.

As per gafter's instructions. This would be a terrific feature for the same reasons as local functions: scoping, keeping together when moving, declaration closer to the usage.

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

I guess my question here is what kind of "local" types are we discussing? Full-fledged types which just happen to be declared and scoped within a method block? Anonymous implementations of an interface or parent type? All of the above? What are some specific use-cases?

From my experience with Java it seems that anonymous implementations are quite common (not even including functional interfaces). Local classes seem relatively rare by comparison as nested classes seem to be sufficient in isolating code at least from outside consumers.

@jnm2
Copy link
Contributor Author

jnm2 commented Mar 7, 2016

Full-fledged types which just happen to be declared and scoped within a method block. I think anonymous implementations are another topic which apparently won't happen (bit sad about that).

One use case that I've had in mind is smart tuples, structs that are one step away from anonymous types but that implement an interface or have decision-making ability of their own. Currently you'd have to declare them outside a method even though they rarely get used in more than one method.

Another use case is returning a private implementation from a method.

Sure, I can simulate this feature with internal (or private in nested classes) using a region, but it would feel so much more clean to scope it more precisely. And declare it near where it is used in the method body. This isn't about external consumers; it's about organizing internals in a cleaner way.

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

Ok. In my opinion, the biggest advantage to local types over nested types is the ability to enclose its parent scope. The question is then how to deal with collisions/shadowing with that scope.

  1. Could you declare a field with the same name as a variable within the local type?
  2. If the local type inherits from a base class that exposes an accessible field what happens if there is already a local variable in scope?
  3. If you use this within the local type, which this are you referring to? Would that depend on what member you might attempt to access from this?

The easy answer would be, "do what Java does." I don't know if that would be the correct answer, though. Java's scoping rules for local classes are somewhat complex.

@jnm2
Copy link
Contributor Author

jnm2 commented Mar 7, 2016

I see the rules as identical to nested types but with more limited scope:

  1. Don't see why it would be helpful to restrict this to be different than nested types.
  2. Need an example, not visualizing this one (possibly answered in 3)
  3. Nested types don't have a reference to the containing type's this. I don't see how local types would be able to maintain a reference to the outer this either. It would be counter-intuitive and it doesn't fit any use cases I can think of. If you need a reference, you pass it through the nested type's constructor; I'd assume the same for a local type. So this would always refer to the instance of the local type.

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

@jnm2

So you don't think that local types should enclose the scope of their parent method? Why should local types be different than any other aspect of C# where lexical scoping is the norm? Without lexical scoping I don't really see much point to local types at all.

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

To note, with local types I'd expect the following to work:

class A { protected char a = 'a'; }
class B { protected char b = 'b'; }

public class C : A {
    private char c = 'c';
    public static char d = 'd';

    public void CreateLocalObject(char e) {
        char f = 'f';

        class Local : B {
            char g = 'g';

            public void PrintVars() {
                Console.WriteLine(g); // Local.g
                Console.WriteLine(f); // local variable f
                Console.WriteLine(e); // local parameter e
                Console.WriteLine(d); // static C.d
                Console.WriteLine(c); // C.c
                Console.WriteLine(b); // B.b
                Console.WriteLine(a); // A.a
            }
        }

        Local local = new Local();      // Create an instance of the local class
        local.PrintVars();              // and call its printVars() method. 
    }
}

If local types also supported "hoisting" as local functions do I would expect that it would continue to work with g defined after Local.

@jnm2
Copy link
Contributor Author

jnm2 commented Mar 7, 2016

If you think it's valuable, I won't argue with that. I just can't think of a particular use case for dual thises, and I do see value in limiting scope and moving the declaration nearer to the usage even without that feature.

What's the mechanism by which each instance of Local accesses a reference to the instance of C, since it's not given a field and injected through the ctor?

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

@jnm2

The same way local functions and lambdas do, the actual locals/fields/whatever are enclosed and promoted to fields on a state machine that is accessible to both the declaring method and the local type.

@alrz
Copy link
Contributor

alrz commented Mar 7, 2016

I think one compelling use case for this is to avoid methods like Observable.Create and passing delegates which is not an attractive option at all, though, I'd prefer anonymous local classes for that specific example.

@jnm2
Copy link
Contributor Author

jnm2 commented Mar 7, 2016

@HaloFour Conceptually that makes sense but mechanically, what does new Local() get transformed to? Is Local given a hidden field and a parameter added to each constructor invisibly?

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

@jnm2

Possibly. What the compiler does now for lambdas that enclose this or member access is that it emits a private nested type that has a field of that type and it assigns that field to this, then it just performs the member access directly. Something like this:

public void DoStuff() {
    string greetings = "Hello";
    Action action = () => Console.WriteLine(greetings + " " + this.ToString());
}

// becomes
private void DoStuff_DisplayClass {
    public C _this;
    public string _greetings;

    public void action() {
        Console.WriteLine(_greetings + " " + _this.ToString());
    }
}

public void DoStuff() {
    DoStuff_DisplayClass temp = new DoStuff_DisplayClass();
    temp._this = this;
    temp._greetings = "Hello";
    Action action = temp.action;
}

I don't know if local functions did this differently, it was mentioned that they could silently insert ref parameters instead of needing to assign fields.

As for local types, I do imagine that if you omit a constructor that one would be created which might accept this or other enclosed state, or if you do specify a constructor that it is silently amended to include those additional parameters. That wouldn't be unlike nested types in Java that automatically get a this parameter even when they're not local types.

@svick
Copy link
Contributor

svick commented Mar 8, 2016

@HaloFour Lambdas can only be passed around as delegates (object reference and MethodInfo pair), which makes referencing the closure easy. But types can be passed as generic type parameters or Type, which don't have anything like that.

Imagine the following methods:

public static T Create<T>() where T : new()
{
    return new T();
}

public static object Create(Type type)
{
    return Activator.CreateInstance(type);
}

And I tried using them with your Local:

Create<Local>();
Create(typeof(Local));

Would that work? If yes, how? If not (because Local wouldn't actually have a parameterless constructor), then I think that would be pretty confusing.

@HaloFour
Copy link

HaloFour commented Mar 8, 2016

@svick

That is an excellent point. In Java a local class can never have a parameterless constructor. It always accepts at least the instance of the parent type as an argument, even if it never uses it, as well as other enclosed variables.

I don't think that lexical scoping would be possible unless the constructor of the local type can be automatically modified by the compiler to accept the enclosed state in some form. In my opinion lexical scoping is a more valuable feature than being able to use the local type via reflection or constrained generics, and with more use cases.

Note that while I think that lexical scoping is important I don't want to hijack this proposal. If simple type scoping is all people really want then so be it. But it is my opinion that the scoping benefits alone aren't enough to warrant such a language change. Every other feature in C# that permits nested declaration enjoys lexical scoping and I think it would be quite confusing if this was the one exception. That said, I wouldn't want to just copy whatever Java does either.

@alrz
Copy link
Contributor

alrz commented Mar 9, 2016

If we simply don't have a name for the local class, then it's impossible for a user to fall into those confusions. Basically, an anonymous type declaration instead of a local type declaration. In @HaloFour 's example, the Local name is just referenced once, so why do you need to name it? And if so, I think existing nested classes are good enough for that use case. In F# we do have object expressions but not local types, right?

@HaloFour
Copy link

HaloFour commented Mar 9, 2016

@alrz Without a name how could you create one outside of a combined declaration/initialization? I think that would fall under #13 and that hasn't gone over well.

@alrz
Copy link
Contributor

alrz commented Mar 9, 2016

@HaloFour #13 syntax doesn't make sense to me, it actually does combine declaration and initialization. I'm talking about (Java) anonymous classes. For parsing issue I'd suggest new class B() because I can't imagine any use case that you would need to combine object initialization and class declaration.

@HaloFour
Copy link

HaloFour commented Mar 9, 2016

@alrz The "explicit" syntax of #13 is pretty much exactly Java anonymous classes except with C# syntax. If you're going to declare the local type but use it elsewhere you'd still need to name that type, otherwise how would you instantiate it?

@alrz
Copy link
Contributor

alrz commented Mar 9, 2016

@HaloFour While you declaring it?

  public IEnumerator<T> GetEnumerator() {
    return new IEnumerator<T> {
      public bool MoveNext() { ... }
      public T Current { .. }

    }
  }

@jnm2
Copy link
Contributor Author

jnm2 commented Mar 9, 2016

@alrz What you are describing is exactly the topic of #13. I'd like to differentiate this topic (allowing types to be locally scoped) from that one (anonymous implementation types).

We already have anonymous locally scoped types. This issue is specifically asking for named locally scoped types with all the capabilities of nested types.

@aluanhaddad
Copy link

I think that scoping(visibility) would be quite useful, but I also agree with @HaloFour that scoping(lexical) is essential. This is very useful in languages like JavaScript and should not be underestimated. Also I think the argument that lexical scoping is the norm in C# combined with the fact that lexical scoping is the best form of scoping there is, is very strong.

I think conflicting references within the inner type could be resolved by qualifying them with this or base where this or base refers to the inner type or its base class. I need to think this over...

@jnm2
Copy link
Contributor Author

jnm2 commented Jun 1, 2016

I think conflicting references within the inner type could be resolved by qualifying them with this or base where this or base refers to the inner type or its base class. I need to think this over...

base must refer to the nested type's base type. We'd need a new keyword, which makes me question the whole magic context thing. If you need a reference to the outer this, why not explicitly declare an owner field in the nested type, pay-for-play? How terrible is that actually? (It feels a bit like dependency injection vs service location. Explicit and visible is better than magic and automatic.)

@HaloFour
Copy link

HaloFour commented Jun 1, 2016

@jnm2

If you need a reference to the outer this, why not explicitly declare an owner field, pay-for-play? How terrible is that actually?

Java introduces a lot of extra syntax to deal with the implicit this of nested and local types. C# already diverged from that path by not having the concept of "instance" v. "static" nested types but requiring that a nested type manually declare and take a reference to its containing type. I think that the same rule for local types would be reasonable. With lexical scoping it would be as simple as declaring a local in the containing method, e.g. var self = this;

@jnm2
Copy link
Contributor Author

jnm2 commented Jun 1, 2016

...requiring that a nested type manually declare and take a reference to its containing type. I think that the same rule for local types would be reasonable.

I'm on the same page here.

With lexical scoping it would be as simple as declaring a local in the containing method, e.g. var self = this;

This could be pay-for-play with a smart compiler, but this would mean all ctors would have to be injected invisibly and instantiation outside the method's lexical scope would be impossible. What if you need to construct and pass a nested type into a method from outside that scope? What about reflection, deserialization?

Also these things become ctor params. Assuming public, internal and private work as expected, a method-private type wouldn't be able to be injected into the public constructor of a method-public type.

Btw field injection could work around all these issues but then you'd have to grep even more magic, worrying about where lexically the type was constructed. Let's not go there.

@HaloFour
Copy link

HaloFour commented Jun 1, 2016

@jnm2

This could be pay-for-play with a smart compiler, but this would mean all ctors would have to be injected invisibly ...

Also these things become ctor params

Exactly, the enclosed values would be passed to a hidden constructor parameter, probably bundled together in a single display reference type. This is the same as with closures today.

Or, if the local type doesn't define a constructor, or the local type doesn't use those enclosed values within a constructor, those values could just be fields defined implicitly directly on the local type.

... and instantiation outside the method's lexical scope would be impossible.

I wouldn't expect that to be possible anyway. A type defined within the scope of a method is not in scope outside of that method. You shouldn't be able to instantiate it or reference it by name anywhere but the method in which it is defined (and any other scopes within that method, e.g. other local types or closures). The actual type name should be compiler generated, mangled and inutterable, just as they are with closure display classes, and just as they are with local functions.

If you need a type that can be referenced between multiple methods you already have normal nested types.

Assuming public, internal and private work as expected, a method-private type wouldn't be able to be injected into the public constructor of a method-public type.

Assuming those accessibility modifiers work as expected where? I'd expect that any accessibility modifiers on the nested type itself would be forbidden. The type is implicitly private.

Btw field injection could work around all these issues but then you'd have to grep even more magic, worrying about where lexically the type was constructed. Let's not go there.

Indeed, let's not. There's no reason to. Handling enclosed lexical scoping through hidden constructor parameters is already established in other languages and works quite well.

@jnm2
Copy link
Contributor Author

jnm2 commented Jun 1, 2016

If you need a type that can be referenced between multiple methods you already have normal nested types.

Correct. Not sure what I was thinking there.

So... last problem is reflection and serialization (JSON.NET et al). How does a serializer instantiate a nested method type? I can see easily 50% of my use cases involving serialization.

@HaloFour
Copy link

HaloFour commented Jun 1, 2016

@jnm2

I imagine that typical reflection scenarios would "just work." The local type should be the same as any other nested type with the exception of some degree of name mangling and possibly any hidden constructor parameters to support closures. I don't think either would impact serialization via JSON.NET.

As for deserialization, I assume that this method accepts a Stream or a string or some other untyped source and you're deserializing within the method to a POCO that isn't referenced outside of that method? With the exception of those hidden constructor parameters I don't see that causing an issue either. I would imagine that if you didn't enclose any scope that said parameters simply wouldn't exist in the constructor so deserialization would just work as expected also. This is exactly how local types work in Java.

@jnm2
Copy link
Contributor Author

jnm2 commented Jun 1, 2016

I would imagine that if you didn't enclose any scope that said parameters simply wouldn't exist in the constructor so deserialization would just work as expected also. This is exactly how local types work in Java.

JSON.NET needs a default ctor. Are you saying a nested type should have a default ctor which calls the invisible injection ctor with default values for the injected params? The net effect would be that when the nested type accesses the method variables, they are default/null if the type was constructed by JSON.NET vs if the type was constructed within the method. That doesn't sound clean to me.

@HaloFour
Copy link

HaloFour commented Jun 1, 2016

@jnm2

I'm saying that as long as the local type doesn't enclose over the method's scope then the constructor would be unaffected. But if the local type does enclose over a local or whatever then the local type would no longer have any parameterless constructors and you'd have to be aware of that when trying to use the type with something like JSON.NET.

In Java if you declare the following:

public void method() {
    final String greetings = "Hello!";
    class Foo {
        void sayHello() {
            System.out.println(greetings);
        }
    }
    Foo foo = new Foo();
    foo.sayHello();
}

It is effectively transformed into:

private static class method$1Foo {
    private final String greetings;

    public method$1Foo(final String greetings) {
        this.greetings = greetings;
    }

    void sayHello() {
        System.out.println(greetings);
    }
}

public void method() {
    final String greetings = "Hello!";
    method$1Foo foo = new method$1Foo(greetings);
    foo.sayHello();
}

@jnm2
Copy link
Contributor Author

jnm2 commented Jun 2, 2016

So the deserialization scenario won't work if you close over a local. Guess that makes sense.

I wonder if there is any way to close over the locals by reference so that they can be written as well as read and synced between nested type instances?

@HaloFour
Copy link

HaloFour commented Jun 2, 2016

@jnm2

Sure. Java made the decision that enclosing locals from the method scope requires that those locals be final, but C# has no such limitation today. I would expect that if C# were to get nested types like this that it would follow the same strategy that it does with lambdas, where it emits a separate reference type as a container for the locals:

public void Method() {
    var message = "Hello!";
    class Foo {
        void SayHello() => Console.WriteLine(message);
    }
    var foo = new Foo();
    foo.SayHello();
}

would be translated into something like this:

private class MethodFooDisplayClass {
    public string message;
}

private class MethodFoo {
    private readonly MethodFooDisplayClass locals;

    public MethodFoo(MethodFooDisplayClass locals) {
        this.locals = locals;
    }

    void SayHello() {
        Console.WriteLine(locals.message);
    }
}

public void Method() {
    var locals = new MethodFooDisplayClass();
    locals.message = "Hello!";
    Foo foo = new Foo(locals);
    foo.SayHello();
}

Although maybe something more clever could be done to avoid the additional allocation for the locals.

@Pxtl
Copy link

Pxtl commented Aug 25, 2016

This looks like to be the only open request for something analogous to Java's anonymous inner classes, so +1 for this. I've been running into this a lot where I have a collection of singletons - that is, a collection where each member is an instance of a different subclass of a common abstract class. In Java this is really easy with anonymous inner classes - basically allowing me to do a full class declaration in the body of a method, one that captures references to the surrounding scope, and instantiate it in the inner-class-declaration expression. Extremely useful. Otherwise, you either have to use reflection to populate the singleton-collection by searching through the classes, or you have to define the classes and list separately and maintain redundant code (and somebody will always forget to add an instance of the class to the singleton-collection).

@jnm2
Copy link
Contributor Author

jnm2 commented Feb 15, 2017

@gafter Should I move this to csharplang?

@gafter
Copy link
Member

gafter commented Feb 15, 2017

@jnm2 Sure, if you want to keep the discussion moving.

@jnm2
Copy link
Contributor Author

jnm2 commented Feb 16, 2017

Issue moved to dotnet/csharplang #130 via ZenHub

@jnm2 jnm2 closed this as completed Feb 16, 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

8 participants