Click here to Skip to main content
15,886,110 members
Articles / All Topics

C# 7 Feature Proposal: Local Functions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
3 Mar 2016CPOL3 min read 13.4K   6
Let’s discuss another of the features that may be coming to the next version of the C# language: Local Functions.

Let’s discuss another of the features that may be coming to the next version of the C# language: Local Functions.

This post discusses a proposed feature. This feature may or may not be released. If it is released, it may or may not be part of the next version of C#. You can contribute to the ongoing discussions here.

Local functions would enhance the language by enabling you to define a function inside the scope of another function. That supports scenarios where, today, you define a private method that is called from only one location in your code. A couple scenarios show the motivation for the feature.

Suppose I created an iterator method that was a more extended version of Zip(). This version puts together items from three different source sequences. A first implementation might look like this:

C#
public static IEnumerable<TResult> 
SuperZip<T1, T2, T3, TResult>(IEnumerable<T1> first,
    IEnumerable<T2> second,
    IEnumerable<T3> third,
    Func<T1, T2, T3, TResult> Zipper)

{
    var e1 = first.GetEnumerator();
    var e2 = second.GetEnumerator();
    var e3 = third.GetEnumerator();
    while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext())
        yield return Zipper(e1.Current, e2.Current, e3.Current);
}

This method would throw a NullReferenceException in the case where any of the source collections was null, or if the Zipper function was null. However, because this is an iterator method (using yield return), that exception would not be thrown until the caller begins to enumerate the result sequence.

That can make it hard to work with this method: errors may be observed in code locations that are not near the code that introduced the error. As a result, many libraries split this into two methods. The public method validates arguments. A private method implements the iterator logic:

C#
public static IEnumerable<TResult> 
SuperZip<T1, T2, T3, TResult>(IEnumerable<T1> first,
    IEnumerable<T2> second,
    IEnumerable<T3> third,
    Func<T1, T2, T3, TResult> Zipper)
{
    if (first == null)
        throw new NullReferenceException("first sequence cannot be null");
    if (second == null)
        throw new NullReferenceException("second sequence cannot be null");
    if (third == null)
         throw new NullReferenceException("third sequence cannot be null");
    if (Zipper == null)
         throw new NullReferenceException("Zipper function cannot be null");

    return SuperZipImpl(first, second, third, Zipper);
}
 
private static IEnumerable<TResult> 
SuperZipImpl<T1, T2, T3, TResult>(IEnumerable<T1> first,
    IEnumerable<T2> second,
    IEnumerable<T3> third,
    Func<T1, T2, T3, TResult> Zipper)
{
    var e1 = first.GetEnumerator();
    var e2 = second.GetEnumerator();
    var e3 = third.GetEnumerator();
    while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext())
        yield return Zipper(e1.Current, e2.Current, e3.Current);
}

This solves the problem. The arguments are evaluated, and if any are null, an exception is thrown immediately. But it isn’t as elegant as we might like. The SuperZipImpl method is only called from the SuperZip() method. Months later, it may be more difficult to understand what was originally written, and that the SuperZipImpl is only referred to from this one location.

Local functions make this code more readable. Here would be the equivalent code using a Local Function implementation:

C#
public static IEnumerable<TResult> 
SuperZip<T1, T2, T3, TResult>(IEnumerable<T1> first,
    IEnumerable<T2> second,
    IEnumerable<T3> third,
    Func<T1, T2, T3, TResult> Zipper)
{
    if (first == null)
        throw new NullReferenceException("first sequence cannot be null");
    if (second == null)
        throw new NullReferenceException("second sequence cannot be null");
    if (third == null)
        throw new NullReferenceException("third sequence cannot be null");
    if (Zipper == null)
        throw new NullReferenceException("Zipper function cannot be null");
 
    IEnumerable<TResult>Iterator()
    {
        var e1 = first.GetEnumerator();
        var e2 = second.GetEnumerator();
        var e3 = third.GetEnumerator();

        while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext())
            yield return Zipper(e1.Current, e2.Current, e3.Current);
    }
    return Iterator();
}

Notice that the local function does not need to declare any arguments. All the arguments and local variables in the outer function are in scope. This minimizes the number of arguments that need to be declared for the inner function. It also minimizes errors. The local Iterator() method can be called only from inside SuperZip(). It is very easy to see that all the arguments have been validated before calling Iterator(). In larger classes, it could be more work to guarantee that if the iterator method was a private method in a large class.

This same idiom would be used for validating arguments in async methods.

This example method shows the pattern:

C#
public static async Task<int> PerformWorkAsync(int value)
{
     if (value < 0)
         throw new ArgumentOutOfRangeException("value must be non-negative");
     if (value > 100)
         throw new ArgumentOutOfRangeException("You don't want to delay that long!");
 
    // Simulate doing some async work
    await Task.Delay(value * 500);

    return value * 500;
}

This exhibits the same issue as the iterator method. This method doesn’t synchronously throw exceptions, because it is marked with the ‘async’ modifier. Instead, it will return a faulted task. That Task object contains the exception that caused the fault. Calling code will not observe the exception until the Task returned from this method is awaited (or its result is examined).

In the current version of C#, that leads to this idiom:

C#
public static Task<int> PerformWorkAsync2(int value)
{
     if (value < 0)
         throw new ArgumentOutOfRangeException("value must be non-negative");
     if (value > 100)
         throw new ArgumentOutOfRangeException("You don't want to delay that long!");
     return PerformWorkImpl(value);
}

private static async Task<int> PerformWorkImpl(int value)
{
     await Task.Delay(value * 500);
     return value * 500;
}

Now, the programming errors cause a synchronous exception to be thrown (from PerformWorkAsync) before calling the async method that leverages the async and await features. This idiom is also easier to express using local functions:

C#
public  static Task<int> PerformWorkAsync(int value)
{
     if (value < 0)
         throw new ArgumentOutOfRangeException("value must be non-negative");
     if (value > 100)
         throw new ArgumentOutOfRangeException("You don't want to delay that long!");
     async Task<int> AsyncPart()
     {
         await Task.Delay(value * 500);
         return value * 500;
     }
 
    return AsyncPart();
}

The overall effect is a more clear expression of your design. It’s easier to see that a local function is scoped to its containing function. It’s easier to see that the local function and its containing method are closely related.

This is just a small way where C# 7 can make it easier to write code that more clearly expresses your design.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Bill Wagner Software LLC
United States United States
Bill Wagner is one of the world's foremost C# developers and a member of the ECMA C# Standards Committee. He is President of the Humanitarian Toolbox, has been awarded Microsoft Regional Director and .NET MVP for 10+years, and was recently appointed to the .NET Foundation Advisory Council. Wagner currently works with companies ranging from start-ups to enterprises improving the software development process and growing their software development teams.

Comments and Discussions

 
QuestionNot interested Pin
William E. Kempf3-Mar-16 9:12
William E. Kempf3-Mar-16 9:12 
I find this feature to be not worth it, TBH. I find the code to be harder to read, though I'll admit that's something one could get used to. However, it doesn't really solve any problem I have, and a lambda could be used instead even if it did solve anything meaningful.

C#
public static IEnumerable<int> Foo(IEnumerable<int> source)
{
	if (source == null) throw new ArgumentNullException(nameof(source));
	Func<IEnumerable<int>, IEnumerable<int>> iter = (IEnumerable<int> s) => s.Select(x => x + 1);
	return iter(source);
}


Lambda's have a bit of overhead, but not enough to warrant a language change like this one. I just don't see a compelling need in this case.
William E. Kempf

AnswerRe: Not interested Pin
Klaus Luedenscheidt3-Mar-16 18:40
Klaus Luedenscheidt3-Mar-16 18:40 
SuggestionRe: Not interested Pin
Member 97000634-Mar-16 5:23
Member 97000634-Mar-16 5:23 
GeneralRe: Not interested Pin
williams20007-Mar-16 2:08
professionalwilliams20007-Mar-16 2:08 
QuestionIt is version for release? Pin
Alexandr Stefek3-Mar-16 5:05
Alexandr Stefek3-Mar-16 5:05 
AnswerRe: It is version for release? Pin
Bill Wagner3-Mar-16 6:26
professionalBill Wagner3-Mar-16 6:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.