Discussion: Local Functions #2930

Closed
gafter opened this Issue May 20, 2015 · 69 comments

Projects

None yet
@gafter
Member
gafter commented May 20, 2015

In #259 it is proposed to add local functions and types. The Roslyn compiler team met today to look at local functions (not local types) in some detail, in preparation for some planned prototyping work by @khyperia. The idea is to be able to define functions in block scope. The C# language design team will consider local functions tomorrow. If this experiment is successful we would look at VB as well. Here are my notes from the compiler team meeting (organized mainly by topic, not chronologically)

Local Functions

We discussed a number of use cases that motivated our investigation:

  • It is very common to write a helper method that is only used from one place, but it makes the code less clear to the reader because the association between the helper function and the thing it is helping is not explicit. Local functions makes the association part of the syntax.
  • An iterator method that validates its arguments (write a non-iterator function to validate the arguments, and then return the result of invoking a local iterator function)
  • A Task-returning method that has no async machinery in the common case where it doesn't need to "go async"
  • You are writing a method that returns an array, but want the syntactic convenience of the iterator method syntax (yield return).

Most of the design questions have seemingly obvious answers.

  • Syntax Trees: New production that is a new kind of block statement
  • Semantics: See below
  • Return type inference: probably allow if function does not reference itself. Need a syntax.

Feature Interactions

  • Caller info attributes on parameters
  • Local functions may be generic
  • Callers may use optional/default/named arguments
  • May capture variables (like a lambda)
  • Definite assignment just like lambda
  • May be self-recursive (if not inferred return type)
  • Inferred return type (if does not reference itself)
  • Conversions, extension methods, operators (not allowed)
  • May use {} or => syntax for function body
  • Is a method group that may be subject to delegate conversion
  • Non-local control transfer (disallow, as we do for lambdas)
  • Iterator functions supported
  • Async functions supported

Scoping Rules

  • Exactly the same as for local variables (therefore no overloading)
  • They may be declared only directly within a block

Parameters:

  • optional/default parameter values
  • ref, out
  • params
  • __arglist
  • extension ("this") not allowed
  • Caller info attributes OK

Modifiers:

  • public
  • unsafe
  • Attributes (perhaps not allowed on the function)
  • async
  • partial
  • static
  • virtual
  • extern (maybe)

Implementation and API

  • In the bound trees, add a new property on BoundBlock for function symbols declared
  • They are MethodSymbols, with symbol.MethodKind==MethodKind.LocalFunction.
  • Probably existing lambda lowering pass can be adapted to lower local functions too

Possible Future Optimization:

  • Capture by copy when possible
  • Capture by ref parameter
  • Inline into call site

/cc @khyperia @MadsTorgersen @jaredpar @agocke @AlekseyTs @VSadov @mattwar @KevinRansom @ljw1004 @AnthonyDGreen

@khyperia khyperia was assigned by gafter May 20, 2015
@gafter gafter added this to the C# 7 and VB 15 milestone May 20, 2015
@gafter
Member
gafter commented May 20, 2015

You are writing a method that returns an array, but want the syntactic convenience of the iterator method syntax (yield return)

That would look something like this

    Foo[] GetFoos(Whatever a)
    {
        IEnumerable<Foo> result() // iterator local function
        {
            yield return a.First;
            yield return a.Last;
        }

        return result().ToArray();
    }
@tmat
Member
tmat commented May 20, 2015

Should we do iterator lambdas in C# first?

@agocke
Contributor
agocke commented May 20, 2015

@tmat No, iterator lambdas don't provide much value -- you end up with a delegate that returns an IEnumerable, which is almost always worse than a local iterator function.

@agat50
agat50 commented May 20, 2015

As i mentioned in https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/6504697-block-code-as-value-another-syntax-for-semicolon would be nice to see

Foo[] GetFoos(Whatever a)
{
    return {
        yield return a.First;
        yield return a.Last;
    }.ToArray();
}

Imho, local functions are using one time mostly, so no needs for name\parameters\return type. If we need multiple invocation, lambda converted to Func\Action is enough already.

@paulomorgado
void M()
{
    var f(int n)
    {
        return (n == 1) ? 1 : n * f(n - 1);
    }

    var x = f(5);
}

Can't the return type of f be inferred on this case?

@paulomorgado

Will expression bodied funtions be allowed?

void M()
{
    var f(int n) => (n == 1) ? 1 : n * f(n - 1);

    var x = f(5);
}
@svick
Contributor
svick commented May 20, 2015

@agat50 I think named local functions are useful, the lambda syntax can be awkward (and doesn't work well with recursion).

Though your suggestion is not without merit either. I've wanted something similar for async blocks executing concurrently (but not necessarily using multiple threads). E.g.:

var task1 = async
{
    Console.WriteLine(await FooAsync());
};
var task2 = async
{
    Console.WriteLine(await BarAsync());
};

await Task.WhenAll(task1, task2);

Though I guess you could achieve a very similar effect with a library method.

@ljw1004
Contributor
ljw1004 commented May 20, 2015

Lambdas work great in VB already for all the use-cases in this thread.

Here's Neal's initial example, which in VB involves less repetition than Neal's C# version since you don't need to write the return type of the lambda. (@agocke you said it's worse than the local function but I don't see any way that it's worse).

Function GetFoos(a As Whatever) As Foo()
    Dim result = Iterator Function()
                     Yield a.First
                     Yield a.Last
                 End Function
    Return result().ToArray
End Function

For @agat50 who liked iterator blocks, here's how you'd do them with iterator lambdas, again involving no more syntactic complexity than what you had:

Function GetFoos(a As Whatever) As Foo()
    Return Iterator Function()
               Yield a.First
               Yield a.Last
           End Function().ToArray
End Function

For @svick yes it works fine with recursion (just not mutual recursion, but honestly by that point I think your code has become too complex):

Sub M()
    Dim f As Func(Of Integer, Integer) = Function(n)
                                             Return If(n = 1, 1, n * f(n - 1))
                                         End Function
    Dim x = f(5)
End Sub

For @paulomorgado yes it can infer the return type of the lambda: (the only thing we didn't yet implement is inferring the return of a method which mentions itself, since this problem doesn't have a general solution, but whatever we come up with for local functions could just as well be applied to local functions)

Sub M()
    Dim f = Function(n As Integer)
                Return If(n = 1, 1, n * 2)
            End Function
    Dim x = f(5)
End Sub

For @svick yes VB already allows what amount to async blocks. The only additional syntactic complexity is a set of parentheses at the end:

Async Sub M()
    Dim task1 = Async Function()
                    Console.WriteLine(Await FooAsync())
                End Function()
    Dim task2 = Async Function()
                    Console.WriteLine(Await BarAsync())
                End Function()
    Await Task.WhenAll(task1, task2)
End Sub

What makes all this possible? (1) Lambda expressions have a default type, just like most other expressions in C#. This would be a desirable feature to add to C# regardless. (2) When you define a lambda which refers to itself, the VB compiler knows that any "use before assignment" error would be incorrect, so it suppresses the error in this case. (3) There are iterator lambdas.

I haven't yet seen examples in this thread where the heavyweight addition of an entire new concept would by syntactically any more convenient than the status quo.

If anyone makes the comment that inner functions would be more efficient than using a lambda? -- strongly disagree. It's up to the compiler what to optimize and what kind of IL to emit. In all these cases the compiler's completely at liberty to emit for the lambdas the same IL as it would for inner functions. Actually, in cases where it doesn't do any capture, I reckon it already more or less does.

@agat50
agat50 commented May 20, 2015

@svick yes, some kind of static Eval\EvalAsync would help (not very handy cause of verbosity and lambda debug(2015 solve last)). My case is more about reorganizing code, not shorter lambda notation, it's useful in some narrow spaced places like exception filtering. We get aggressive inline code, small temp vars scope. Your case is interesting too, i didn't think much about async blocks.

Simple code replacement (my proposal)

var i = 
{
    await Task.Delay(1000);
    return 3;
};

// equals

int i;
{
    await Task.Delay(1000);
    i = 3;
}

Or actually create anonymous method

var i = await {
    await Task.Delay(1000);
    return 3;
}.ConfigureAwait(false);
@agocke
Contributor
agocke commented May 20, 2015

@paulomorgado Expression bodies will be allowed.

Factorial is an example of a recursive function whose return type could be inferred. However, the general case requires H-M and during the meeting we agreed that we didn't want to do that kind of non-local analysis right now.

@agocke
Contributor
agocke commented May 20, 2015

@ljw1004 You're cheating by using VB syntax ;) An iterator lambda involves an unnecessary delegate and invocation and additional syntax for type annotations in C# . Local functions seem substantially better.

Edit: Rereading, I see you're proposing more features to get around this, like default delegate types and, I assume, implicit delegate conversion in C#. That may provide the benefits of iterator local functions, but I would regard those as out of scope for the current discussion.

@HaloFour

Yeah, I think eliminating the delegate invocation would be a great benefit. Being able to enclose the local scope through implicit parameters sounds like it could yield good performance benefits as well.

I do think that there is value for C# to support iterator lambdas. Sometimes you just want to pass an iterator inline to a LINQ method like SelectMany.

Any thoughts to blurring the line between local functions and lambdas? For example, if the delegate instance is never combined, invoked dynamically nor leaves the scope of the method, why not optimize by converting to a local method?

@agocke
Contributor
agocke commented May 20, 2015

@HaloFour I think there's definitely some room for improvement in optimization for lambdas as well.

@orthoxerox

Is there a reason why a static modifier is not allowed? If the function doesn't close over any variables it should be rewritten as a static function by the compiler anyway; explicitly marking it as static would allow us to declare that we want the compiler to verify we're not accessing object or method state in it.

@HaloFour

@orthoxerox I don't think it makes sense to conflate the static modifier as a declaration of what the local function can or can't enclose. The compiler could always emit a static method even if it did close over this members by passing it silently as an argument.

@gafter
Member
gafter commented May 21, 2015

@paulomorgado

Will expression bodied funtions be allowed?

Read the discussion. It is proposed.

Can't the return type of f be inferred on this case?

Read the discussion. Inferred return type and self-use are allowed but mutually exclusive.

@VSadov
Member
VSadov commented May 21, 2015

Is there a specific reason to not allow local extension methods?

@erik-kallen

Wouldn't almost all use cases be solved by just assigning a lambda to var? Define this to use an appropriate System.Func delegate type. Example:

int M() {
    var add = (int a, int b) => a + b;
    return add(2, 3);
}
@aluanhaddad

Thank you for the update on this proposal. I have wanted local functions in C# for a while and it sounds like the feature design is progressing very nicely.
I agree with @HaloFour that it would be nice if iterator lambdas were supported, but that seems something that would ultimately be a separate feature.

@erik-kallen This can already be done today (without the pleasant type inference):

int M() {
    Func<int, int, int> add = (a, b) => a + b;
    return add(2, 3);
}

I actually use this pattern sometimes, but it has many drawbacks:
It is has a performance penalty, it cannot be generic, it takes a dependency on a BCL type, and it does not allow for recursion without ugly incantations, etc.

@erik-kallen

@aluanhaddad I also use the Func<int, int, int> x = (a, b) => a + b pattern today, but I think it's quite ugly (and it requires specifying the return type, which could be inferred).

As for recursion, I think it would absolutely make sense to change the spec so the variable being assigned to can be used inside the lambda expression. I am aware that there is some kind of esoteric assignment that can cause the variable to be used before it is assigned, but that could be fixed by saying that var x = (int a, int b) => x(a - 1, b); is equivalent to Func<int, int, int> x = null; x = (a, b) => x(a - 1, b).

As for performance, if this is really a concern, the compiler could notice that the delegate can never escape the function and generate a local method; we don't really need additional syntax.

@MgSam
MgSam commented May 21, 2015

While I agree with Lucian that a combination of features would make lambdas able to cover a lot of the use cases here, I think local functions are useful regardless because they'll support a lot more than lambdas can; namely generics, default and optional parameters, and params.

At the end of the day, I think whatever solution is ultimately designed should fulfill the use cases illustrated in the proposed spec.

@HaloFour

Noodle bakin' time:

How about local functions within lambda bodies? 😉

@gafter
Member
gafter commented May 24, 2015

@HaloFour Yes, as you can see from the original issue

[X] May use {} or => syntax for function body

@khyperia
Contributor

@HaloFour if your question meant within lambdas, not having an ArrowExpressionClause itself, like so:

Action foo = () => {
    void Bar()
    {
    }
}

then I don't see why not, seeing as the proposal states this, and lambdas can have blocks as bodies:

They may be declared only directly within a block

@bbarry
bbarry commented May 26, 2015

Would you be able to make PInvoke calls local (why allow extern without attributes)?

public IconFile GetIcon(string iconResourceFile, int index)
{
    [DllImport("shell32.dll")]
    extern IntPtr ExtractIcon(IntPtr hInst,
                              [MarshalAs(UnmanagedType.LPStr)] string lpszExeFileName,
                              uint nIconIndex);

    ...
}
@KrisVandermotten

The discussion about whether lambda's are sufficient for the proposed use cases seems rather strange to me, as one of those use cases is an iterator method that validates its arguments. We need some kind of new capability there.

As I understand it, the proposed syntax would be something like:

public static IEnumerable<int> NumbersUpTo(int n)
{
    if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));

    IEnumerable<int> NumbersUpToCore(int n)
    {
        for (int i = 0; i <= n; i++) yield return i;
    }

    return NumbersUpToCore(n);
}

This does seem rather verbose and unintuitive to me. Certainly, some of the points raised could help, such as inferring the return type, allowing anonymous methods, etc. That could lead to, e.g.

public static IEnumerable<int> NumbersUpTo(int n)
{
    if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));

    return (int n) => { for (int i = 0; i <= n; i++) yield return i; }(n);
}

But I must admit that does not seem to make it more intuitive.

This example raises also a name scoping question: can I use n as the name of the parameter of the local method?

The use case for a Task-returning method that has no async machinery in the common case is very similar, and it would be very good to have a solution for both of them that is very intuitive to use. I have a feeling that local methods may not be the intuitive solution.

What about something like:

public Task<int> GetSomeNumberAsync(string someParameter)
{
    if (someParameter == null) throw new ArgumentNullException(nameof(someParameter));

    async
    {
        return 5 * await WhateverAsync(someParameter);
    }
} 

and

public static IEnumerable<int> NumbersUpTo(int n)
{
    if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));

    iterator
    {
        for (int i = 0; i <= n; i++) yield return i;
    }
}
@qrli
qrli commented May 28, 2015

I am also in the camp that improving other features may solve it better than introducing local functions.

It is largely duplicate with lambda, in syntax. Whether a delegate is created can be optimization detail. E.g. in many script languages, e.g. JavaScript, Python, Lua, function f() {} and var f = () => {} are basically the same thing. So it is better to improve the lambda syntax, and there will be no new rule about captured variables either.

We already got to choose between ()=>{} and delegate {}, it is better not to add yet another choice for almost the same thing.

@bondsbw
bondsbw commented Jun 4, 2015

@KrisVandermotten By the time you got to either of the two changes in NumbersUpTo, I don't really see the benefit at all. Unless I'm mistaken, the result is identical to simply removing the new syntax:

public static IEnumerable<int> NumbersUpTo(int n)
{
    if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));

    for (int i = 0; i <= n; i++) yield return i;
}

Same for async:

public async Task<int> GetSomeNumberAsync(string someParameter)
{
    if (someParameter == null) throw new ArgumentNullException(nameof(someParameter));

    return 5 * await WhateverAsync(someParameter);
} 

Unless I'm mistaken, the only difference is in name scoping. But that can be taken care of in both instances by just the curly braces, not even needing the additional keyword.

@HaloFour
HaloFour commented Jun 4, 2015

@bondsbw

Actually they are quite different. The two methods as you have written them do not perform argument validation as you might expect.

In the first case, the iterator, none of the function body is executed until the caller calls MoveNext on the enumerator. This can result in the validation exception being thrown quite far away from where the validating method is called. If you passed that IEnumerable<int> to a LINQ expression it could appear that the validation occurs deep within the bowels of LINQ. That's not likely what you intended.

In the second case, the async method, to the caller it will appear that the method returned successfully with a Task<int>. That Task<int> will be faulted with the exception, but the method itself will not throw.

Inner functions would serve as a way of performing validation and allowing those methods to throw immediately. This is commonly accomplished through private helper methods currently.

@bondsbw
bondsbw commented Jun 5, 2015

@HaloFour I see, I didn't consider that syntax to have the same semantics as an embedded lambda, but that makes total sense.

The async/iterator syntax now reminds me of the anonymous delegate { ... } syntax without parameters (or, when the parameters exist but are unused). If we go that route I think I would like to consider the implications of extending it beyond local functions, for use anywhere a compatible lambda would make sense, i.e. a lambda that returns Task<T> or IEnumerable<T>.

@bondsbw
bondsbw commented Jun 5, 2015

And then I'd like to see iterator methods with this syntax, for completeness and consistency:

public static iterator int NumbersUpTo(int n)
{
    //...
    yield return i;
    //...
}

which would be identical to

public static IEnumerable<int> NumbersUpTo(int n)
{
    //...
    yield return i;
    //...
}
@svick
Contributor
svick commented Jun 5, 2015

@bondsbw I think it would be really confusing if the declared return type of a method (here int) was different from the actual return type (IEnumerable<int>). And it doesn't increase consistency, async methods have to explicitly declare that they return Task<T>.

Also, how would you specify that the method returns IEnumerator<int> using your proposed syntax?

@HaloFour
HaloFour commented Jun 5, 2015

The only reason that async methods ended up requiring an async keyword was that there are scenarios in which attempting to determine the contextual usage of the await keyword would be ambiguous. Adding async to the method signature tells the compiler that await cannot be used as a type or variable name within the method and eliminates that source of ambiguity. Iterators, which were added first, didn't have that problem as there were no circumstances where yield return <expression> was legal in C# 1.x.

@bondsbw
bondsbw commented Jun 5, 2015

Good points.

@paulomorgado

@HaloFour, are there any circumstances where return await <expression> was legal in C# 1.x?

I think they went over the top by not introducing an iterator modifier in C# like thy did for VB. I think it improves readability.

@MgSam
MgSam commented Jun 10, 2015

I don't remember where, but I believe I've read Eric Lippert say before that he regrets that they didn't require iterator in the signature of iterator methods. It definitely would be nice to be able to see up front in the method signature whether the method is an iterator function, especially in light of the fact that this is the design async went with.

@HaloFour

@paulomorgado

No, but I believe that await <expression> could be ambiguous.

Here's an article that discusses it in some depth:

http://blogs.msdn.com/b/ericlippert/archive/2010/11/11/whither-async.aspx

@whoisj
whoisj commented Jun 10, 2015

👍 on an iterator keyword for improving readability. Preserving the IEnumerator<T> return types seems to make sense from a "you don't have to foreach the result, but you ought to" point-of-view.

@paulomorgado

@HaloFour, forgot that one. Not really any await <expression> but specificaly await(<expression>), right? I can be confused with a method or delegate invocation.

@HaloFour

At this point does it really matter? Sure, a new iterator keyword could be applied to new circumstances, such as iterator lambdas, but we're left with the fact that with existing iterator methods it would have to be optional. It's easy to redesign in hindsight.

@paulomorgado Not to mention you can stuff an await in the middle of any other expression.

@whoisj
whoisj commented Jun 10, 2015

@HaloFour it could easily be required with a newer version of the run-time. It's such a straight forward change that automation could/should be included in the IDE to support the change.

Regardless, it's fancy semantic sugar and adds little value. It'd be a "nice-to-have" at best, but clarity is clarity and that's nearly always a good thing.

@paulomorgado

@whoisj, the runtime has nothing to do with iterators. It's a pure compile feature.

But a future language version could make it mandatory, beacuse it feels strange the it's needed somewhere and not everywhere.

@whoisj
whoisj commented Jun 10, 2015

Sure "language version", that's what I was getting at.

Sorry, I'm horrid with terminology. 😊

@HaloFour

I think that it should be left-alone where it is. If iterator lambdas become a thing then requiring it there makes sense. To have a newer compiler all of a sudden fail to compile existing code is a bad idea, despite whatever tools can make that happen. That's expecting an IDE to be involved, mass check-out/edit/merge, etc. And, yes, it adds clarity, but no other benefit to the language. I think it's best to just accept that it could have been designed better given new knowledge but it is what it is.

And I want to take a moment to thank the C# team for being so careful about backward compatibility. I feel bad for anyone working with Apple Swift to find that their do loops have been taken away from them.

@paulomorgado

I'm all for not breaking backward compability, but the only reason to move forward in the language version is to use new features that are not backward compatible.

This could be just a warning with a solution-wide fix.

@HaloFour

C# doesn't have a history of breaking existing syntax with new language versions. If anything, they've made quite painstaking effort to allow existing syntax to function just fine unless you specifically opt-in to scenarios where an alternate syntax is required (as is the case with async methods). You can disable the functionality of inferred variables and dynamic typing just by adding a System.var or System.dynamic type to your project as the compiler team did not want to break any existing projects on the off-chance that someone named their class one of those new keywords.

Even if a new version of C# adopts an iterator keyword, which I would agree is a good idea, I don't see why it should be made required breaking the existing iterator method syntax. Like I said, for new cases like potentially iterator lambdas I would be fine making it absolutely required, and I'd be fine with them being optionally allowed for existing iterator methods for the sake of clarity and consistency. I don't even think that a warning would be appropriate given that the compiler team has already made statements to the effect that using the new compiler should not emit warnings for existing legal code as breaking existing legal builds (including those that may treat warnings as errors) is not an option that they are considering.

So, for async sequences would we need the following signature? 😄

protected internal static async iterator IAsyncEnumerable<int> Foo() { ... }
@aluanhaddad

For all of the reasons that @halofour mentioned, I do not think that existing methods that are implemented using iterators should require an additional qualifier to compile against new versions of the language. Additionally, I find the introduction of an iterator keyword for the sake of clarification to be somewhat off the mark. The use of an iterator is often an implementation detail that the consumer of the method is not required to consider. Why muddy an API with additional keywords? If the caller should consume the result differently, that fact may be better communicated through a suggestive name or enforced by a different return type. I understand that in the case of async, the modifier had to be introduced to remove potential ambiguities, but this should be seen as separate from the recommended naming conventions for async methods. In the former case the async keyword is placed on the method to enable awaiting within the implementation of the method, while in the latter the method is sufficed with Async to convey something which is relevant to the caller, e.g. that it may have certain performance characteristics.

@paulomorgado

@aluanhaddad like the async modifier, the iterator modifier has no effect in the caller. The caller doesn't even see it.

For backward compatibility, the iterator modifier should not be required on places where it isn't required today. But it should be optional because it improves readability of the iterator method. And, in the future, it might even be required to disambiguate.

@aluanhaddad

@paulomorgado what is being disambiguated? I'm all for being able to write iterator lambas but I'm uncertain of the value of its becoming ultimately required on iterator methods in some future version of the language. Adding a keyword to enable iterator lambdas makes sense if the feature requires it, but annotating existing iterator methods seems to be an orthogonal goal. I see the argument for requiring it for the sake of consistency, but I'm not sure that's a good enough reason given that there will be other differences, such as return type inference, between local and non local functions.

@paulomorgado

Nothing. That's why I wrote future.

Partly because of parity with Visual Basic, partly because I like it!

@bondsbw
bondsbw commented Aug 15, 2015

Assuming local functions can be defined within a constructor, consider allowing them to be accessed when calling the base constructor:

public DerivedClass() : base(myFunc())
{
    bool myFunc()
    {
        ...
    }
}
@HaloFour

@bondsbw

I wouldn't imagine that myFunc would be in scope at that point. If the compiler would emit the local function as static, in that it doesn't attempt to reference this or any instance members then I don't see why not, other than the scope issue. Attempting to access the instance seems like a big no-no to me since the base will not have been fully initialized. It even bothers me that C# permits virtual calls from within a constructor, a departure from C++. Do you have a specific use case for this?

@orthoxerox

@HaloFour there's a common situation when you want to preprocess some parameters before passing them to the base constructor. Sometimes this preprocessing is too lengthy to be written inline in the base call, and it looks weird extracting it to a standalone static function that isn't used anywhere else.

@bondsbw
bondsbw commented Aug 15, 2015

@HaloFour:

@orthoxerox is right on. This is related to #3703. It's already possible to use helper methods, but only static references and parameters may be passed as arguments to the base constructor. It seems those same restrictions would be reasonable, and using a local static function for that purpose would align with the goals of this feature.

@HaloFour

@bondsbw @orthoxerox Alright, just wanted to clarify the use case. There are a couple of gotchas in that idea. First is the scope situation I mentioned earlier. Normal identifiers declared in the constructor body aren't in scope at that point. I doubt local functions would even permit use prior to declaration within the same scope. Local constants don't and they're probably the closest analog. Second is how exactly the function would be implemented. It would need to be static and it would probably have to implicitly define ref parameters to the enclosed variables.

public Bar(int x, int y) : base(myFunc()) {
    int myFunc() {
        return (x + y++);
    }

    int z = myFunc();
}

// would be equivalent to

public Bar(int x, int y) : base(myFunc(ref x, ref y)) {
    int z = myFunc(ref x, ref y);
}

private static int myFunc(ref int x, ref int y) {
    return (x + y++);
}

But then that makes you wonder about the following and whether it would/should be possible?

public Bar(int x, int y) : base(myFunc()) {

    string greeting = "Hello!";
    int myFunc() {
        Console.WriteLine(greeting);
        return (x + y++);
    }
}

Based on the scoping issues alone I doubt I could see local functions implemented in a way to support use by constructors to call other constructors.

@bondsbw
bondsbw commented Aug 15, 2015

@HaloFour I would think that the only cases that should be supported for use in calling the base constructor are when the static local function does not reference anything outside of its scope and its own parameters. Neither of your examples would be legal.

Without thinking it through completely, the basic idea that a static local function can access local variables really feels like a design smell if not outright bad practice. Local static functions should probably be limited from accessing anything that is not static (just like existing static constructs), including local variables (but local static functions would be fair).

@HaloFour

@bondsbw That doesn't resolve the scope issue, which is frankly the significantly bigger problem.

Aside that, reading the original proposal above it appears that there is no intention of supporting static local functions. Whether or not they would be emitted as static would be an implementation detail. The proposal also specifically covers enclosing the local scope. It would have to in order to support enclosing the parameters anyway, and any local function or closure can access any identifier declared before it.

At best you'd need a requirement that the local function(s) are the first thing to appear in the constructor body. That would require some specific exceptions to the C# language to permit referencing an identifier before it has been defined, but that would at least prevent the problem of enclosing state that does not yet exist. But I just really don't see this use case being covered by local functions.

@Jes28
Jes28 commented Aug 20, 2015

Just one suggestion

Allow to write function not only before it usage, but allow just declare it in inner scope so it not be visible from outside.

private Single SomeBigFunction( ... )
{
    //Some big logic using inline finctions

    Single Calc1( Single param1, Single apram2 ){...}
    Single Calc2( Single param1 ){...}
    String Convert( Single[] data, String pattern ){...}
}

We always put public Methods first and private (internal implementation) last, so i want be able to put in same order inline functions.

Just syntax sugar to convert from this:

private Single SomeBigFunction( ... )
{
    //Some big logic using next tree finctions    
}
private    Single Calc1( Single param1, Single apram2 ){...}
private    Single Calc2( Single param1 ){...}
private    String Convert( Single[] data, String pattern ){...}

to this:

private Single SomeBigFunction( ... )
{
    //Some big logic using inline finctions

    Single Calc1( Single param1, Single apram2 ){...}
    Single Calc2( Single param1 ){...}
    String Convert( Single[] data, String pattern ){...}
}
@HaloFour

@Jes28

I don't like the idea of applying "hoisting" to local functions in C#. It is considered bad practice in JavaScript (on the list of "Bad Parts" in Douglas Crockford's book). This is especially true given that local functions are to enclose the local scope of the "parent" function. Where the function is defined will matter.

@aluanhaddad

@Jes28 the proposal states that local variables will be captured as they are by lambda expressions / anonymous functions. Allowing the declaration order to change would prevent this making the feature significantly less useful and powerful. Anyway everything declared in a method body is private, scoped to an invocation.

@gafter gafter closed this Sep 13, 2015
@gafter gafter modified the milestone: C# 7 and VB 15 Nov 26, 2015
@gafter
Member
gafter commented Apr 17, 2016

@agocke There is a checklist here for local functions that you might find useful.

@GSPP
GSPP commented Jul 2, 2016

It would be nice if var parameters were allowed. It would be handy to be able to process anonymous types from LINQ queries e.g.

var myQuery = ... select new { X = 1 };

void ProcessItem(var item, string mode) { ... }

foreach (var item in myQuery) {
 if (...)
  ProcessItem(item, "mode1");
 else
  ProcessItem(item, "mode2");
}

This code is contrived to demonstrate that a local functions with var parameters would be very useful. The function has two callsites. It's not possible to write the computation inline in the for loop without 2x duplication.

var parameters are not normally allowed but since local functions are not exposed to the outside world it seems this could be done more easily here. The same reasoning applies to var returns.

@svick
Contributor
svick commented Jul 2, 2016

@GSPP Couldn't you use a tuple instead of an anonymous type? That way, you wouldn't need var.

@aluanhaddad

@GSPP if that scenario doesn't work there's a big problem. I'm not sure that var parameters would solve this issue but something should definitely be done to address it.

@svick that's true, but local function should have access to the information in their enclosing scope, that should include type information. Otherwise they become less valuable.

I'm not sure how this can be expressed however. Var parameters don't seem like they would make any difference. Something like decltype would be needed.

@mcintyre321
mcintyre321 commented Jan 10, 2017 edited

The hoisting is very weird to c# thinking:
localfunction
I feel like in c#, code using a particular variable shouldn't be callable until after the variable is defined.

@HaloFour

@mcintyre321

The local function isn't a variable. It's more like a member scoped to the function.

@khyperia
Contributor

@mcintyre321 This is a known bug that has been fixed: #15298

@mcintyre321

thanks! Linqpad must be using a not-quite-bleeding-edge compiler!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment