DEV Community

Mark Clearwater
Mark Clearwater

Posted on • Originally published at blog.csmac.nz on

Looking Back on C# 7: Local functions

Looking Back on C# 7: Local functions

With C# 8 on our doorstep, I figure it is a good time to reflect on recent additions to the language that have come before. There are some great improvements you may have missed, some that I really enjoy using, and some I consider have reached canonical usage status that I think are all worth some reflection.

Lambdas - a recap

All the way back in C# 3 we were given lambdas. These are anonymous functions that can be passed around as what are essentially function pointers.

"Anonymous" refers to the fact that the function has no name, and is not tied to an actual class or instance. The term comes from functional programming.

In terms of implementation details, there are two categories of lambdas, those which stand alone and are pure functions, and those that have a captured scope, known as a closure. Closure, again, is a functional programming term. We capture variables from the scope of the parent and encapsulate them into this anonymous function instance.

Pure functions can easily be refactored into a public static function in a static class very easily, and the compilation is very similar.


static Action<int> GetRandomValueFunction()
{
    Console.WriteLine("Use static value for counter: {0}", counter);
    return () => 5;
}

// These two methods are the same as the above
static Action<int> GetRandomValueFunction()
{
    Console.WriteLine("Use static value for counter: {0}", counter);
    return StaticValueFunction;
}

static int StaticValueFunction()
{
    return 5;
}

There is no simple construct equivalent for closures. Hopefully, by example, we can see how these transform into simple static methods anyway. (In an example stolen from SO)

 static Action CreateShowAndIncrementAction()
{
    Random rng = new Random();
    int counter = rng.Next(10);
    Console.WriteLine("Initial value for counter: {0}", counter);
    return () =>
    {
        Console.WriteLine(counter);
        counter++;
    };
}

Given the above C# code, the compiled code will more closely resemble the below:


static Action CreateShowAndIncrementAction()
{
    ActionHelper helper = new ActionHelper();        
    Random rng = new Random();
    helper.counter = rng.Next(10);
    Console.WriteLine("Initial value for counter: {0}", helper.counter);

    // 
    return helper.DoAction;
}

class ActionHelper
{
    public int counter;

    public void DoAction()
    {
        Console.WriteLine(counter);
        counter++;
    }
}

Because the compiler generates these, it also controls access and visibility, so that you cannot actually access it in this way from your code directly. But it does all this automatically for you, and with fewer lines of code to achieve the same logical execution. It is also clearer from the lambda version that the calling method is the owner of the function, and no one else can or should share it.

Clearly, lambdas reduce our lines of code and are superior.

working with lambdas

Sometimes, though, your lambdas get complicated. When you have big complicated lambdas inside LINQ pipelines, it gets messy fast.

public List<Widget> GetWidgetsByPlumbob(string plumb, int restrictionNumber)
{
    var restriction = _store.GetRestriction(restrictionNumber);


    // This `where` clause is only going to get more complicated from here.
    _store
        .GetWidgets()
        .Where(w => (w.PlumbBob.StartsWith(plumbob) || w.PlumbBob.EndsWith(plumbob)) && w.Restriction == restriction)
        .Select(Map)
        .ToList();
}

We have a few options prior to C# 7:

public List<Widget> GetWidgetsByPlumbob(string plumb, int restrictionNumber)
{
    var restriction = _store.GetRestriction(restrictionNumber);

    _store
        .GetWidgets()
        .Where(w => Filter(w, plumb, restriction))
        .Select(Map)
        .ToList();
}

private bool Filter(Widget widget, string plumb, Restriction restriction)
{
    return (w.PlumbBob.StartsWith(plumbob) || w.PlumbBob.EndsWith(plumbob)) && w.Restriction > restriction;
}

There are a couple of problems with this. We are now passing all the parameters through to the nested function. This is fine, but from a maintenance point of view, we might add more parameters and we now end up maintaining two signatures every time we change.

Also, we open up for another function to start using the "sharable" filter function. Now we would be coupled to that new function. We can't change our own filter without affecting that other new function, and that adds brittle fragility. Sometimes duplicating logic that has different reasons to change is worth the duplication, but the architecture of this code does not guard against this.

Another example is to pull the lambda into a variable:

public List<Widget> GetWidgetsByPlumbob(string plumb, int restrictionNumber)
{
    var restriction = _store.GetRestriction(restrictionNumber);

    var filter = w => (w.PlumbBob.StartsWith(plumbob) || w.PlumbBob.EndsWith(plumbob)) && w.Restriction == restriction);

    _store
        .GetWidgets()
        .Where(filter)
        .Select(Map)
        .ToList();
}

Looking at the example, it adds some simplification. Because like the original example this lambda is a closure, the lambda can match the expected signature of the Where LINQ Extension.

However, sometimes this approach causes issues with type interpolation. That is, if you pull out into a variable, often you can't use var or need to add explicit type casting to help the compiler out, or it will not compile.

I don't think we have actually helped with the readability as much as we would like. It may not be obvious on the first pass of reading the function that this is a lambda, not a statement. This is also a simple example, and they can get more complex fast.

Put functions inside your functions!

With C# 7, we can now use a nested function. This gives us the benefits of not polluting the namespace of the class, while also making it more readable. It also makes it clearer that the function is owned by the caller as the only consumer.

public List<Widget> GetWidgetsByPlumbob(string plumb, int restrictionNumber)
{
    var restriction = _store.GetRestriction(restrictionNumber);

    bool Filter(Widget widget)
    {
        return (w.PlumbBob.StartsWith(plumb) || w.PlumbBob.EndsWith(plumb)) && w.Restriction > restriction;
    }

    _store
        .GetWidgets()
        .Where(Filter)
        .Select(Map)
        .ToList();
}

Our compiler now ensures no one else can use this function. It is only callable from this method and gives the reader the knowledge that this is a specific implementation detail for this function only and not a shared common piece of logic. (Encapsulation.)

In this example it also still allows us to use the simplified Where call.

The best example of where this is really useful is recursion.

Often a recursive algorithm has a bootstrap function, that then calls the recursive part. Let's print a tree of items with indentations.

public static void PrintLines(TextWriter out, Tree items)
{
    out.WriteLine(items.Title);
    foreach(var node in items.Children)
    {
        Print(out, node, "");
    }
}

private void Print(TextWriter out, TreeNode node, string indent)
{
    out.WriteLine("{0}{1}", indent, items.Title);
    if(node.HasChildren)
    {
        foreach(var node in items.Children)
        {
            Print(out, node indent + " ");
        }
    }
}

For simplicity you can now write this:

public static void PrintLines(TextWriter out, Tree items)
{
    void Print(TreeNode node, string indent)
    {
        out.WriteLine("{0}{1}", indent, items.Title);
        if(node.HasChildren)
        {
            foreach(var node in items.Children)
            {
                Print(out, node indent + " ");
            }
        }
    }

    out.WriteLine(items.Title);
    foreach(var node in items.Children)
    {
        Print(node, "");
    }
}

This example may not reduce lines of code by much, but the cognitive load of the encapsulation can be hugely beneficial when in a class with more service methods as well.

Summary

The cliché "another tool in the toolbelt" comes to mind but this is certainly that, and sprinkled through code strategically can really help with readability and maintainability. Not a "use often" but certainly something I can and have used in my dotnet apps.

Top comments (0)