Refactoring variables to lambda expressions

Arialdo Martini — 2/08/2022 — SICP C# Functional Programming

Variables are just syntactic sugar for lambda expressions: they are not stricly necessary and ideally C# could happily live without them. Even the reseved word var could be eliminated.

In this post we will use ReSharper for refactoring variables away, transforming them to anonymous lambda expressions.

That’s a trick that will come in handy for deriving the Y Combinator. I wrote about this topic back in 2018, in the excitement of reading Structure and Interpretation of Computer Programs.

SICP claims that:

“[A variable] is simply syntatic sugar for the underlying lambda expression”

where “syntatic sugar” is defined as:

“convenient alternative surface structures for things that can be written in more uniform ways”

In other words, the claim is that variables make code sweeter to write and read, but they add no expressive power.
Let’s go straight to the code.

An example

Here’s a snippet using 3 variables:

[Fact]
void variables_are_used()
{
    var amount = 7;
    var product = "beers";
    var price = 3;

    Assert.Equal(
        "7 beers cost 21 EUR",
        $"{amount} {product} cost {amount * price} EUR");
}

Squeezing your eyes, you could see how this is equivalent to the (more convoluted):

[Fact]
void variables_are_replaced_with_lambdas()
{
    Assert.Equal(
        "7 beers cost 21 EUR",
        new Func<int, string, int, string>(
            (price, product, amount) =>
                $"{amount} {product} cost {amount * price} EUR")
        (3, "beers", 7)
    );
}

where there are no variables at all.
By the end of this post you will learn how to get from the former to the latter implementation, only applying refactoring moves.

Let me be clear: the latter form is way less readable than the original one, and by no means better.
Yet, the transformation to derive it is a not obvious refactoring move that can come in handy, especially when playing with functional programming.

Also, it’s fun to derive. So, why not?

Variables and named methods

Let’s start from a very prosaic equality: any variable can be replaced by a parameterless constant function.

In:

[Fact]
void variable_is_used()
{
    var foo = "bar";

    Assert.Equal(
        "bar",
        foo);
}

var foo = "bar" can be replaced with string foo() { return "bar"; }, getting to:

[Fact]
void variable_is_replaced_with_method()
{
    string foo() =>
        "bar";

    Assert.Equal(
        "bar",
        foo());
}

That’s insultingly trivial, isn’t it?
Bear with me. Let’s see what happens if we use R# to make that foo() function an anonymous lambda.

By the way: getting from var foo = "bar"; to string foo() => "bar" is by itself a refactoring; it’s indeed a matter of applying Extract Method on "bar" and of inlining foo with Inline Variable.

From Named Local Function to Anonymous Lambda

It’s much more interesting to see what happens when we convert foo() to an anonymous lambda.
We can proceed as follows:

Move value away

Select "bar" and apply Extract Method

[Fact]
void variable_is_replaced_with_method()
{
    var foo = Temp();

    Assert.Equal(
        "bar",
        foo);
}

private string Temp() =>
    "bar";

Ideally, we could have done the same with Extract Local Function: for some reasons, though, R# would not be able to perform the next step.

Create the Func

Extract Temp (not Temp(), only Temp, without the ()) in the expression var foo = Temp() as a variable with Introduce Variable. This is the crucial step: it will convert a method to an equivalent variable of type Func<T>:

[Fact]
void variable_is_replaced_with_method()
{
    Func<string> temp = Temp;
    var foo = temp();

    Assert.Equal(
        "bar",
        foo);
}

private string Temp() =>
    "bar";

Inline everything

Apply Inline Variable, in order, to Temp, to temp and finally to foo

[Fact]
void variable_is_replaced_with_anonymous_lambda()
{
    Assert.Equal(
        "bar",
        ((Func<string>)(() => 
            "bar"))
         ());
}

Here we are. Both the variable and the named function have disappeared, leaving us with just an anonymous lambda.

Notice the trailing (): this invokes the parameterless anonymous lambda, feeding it no value, and getting back "bar".

Two steps back

To understand why we got (), it is useful to review what we got when we extracted Temp():

[Fact]
void variable_is_replaced_with_method()
{
    var foo = Temp();

    Assert.Equal(
        "bar",
        foo);
}

private string Temp() =>
    "bar";

Temp() is parameterless, which generates a parameterless lambda. If it had a parameter, also the final anonymous lambda would have had one.
Let’s try.

Add a parameter

We can promote "bar" to a parameter, applying Introduce Parameter:

[Fact]
void variable_is_replaced_with_method()
{
    var foo = Temp("bar");

    Assert.Equal(
        "bar",
        foo);
}

private string Temp(string value) =>
    value;

Create the Func

Like before, extract Temp (without the ("bar") part) in the expression var foo = Temp("bar")

[Fact]
void variable_is_replaced_with_method()
{
    Func<string, string> converter = Temp;
            
    var foo = converter("bar");

    Assert.Equal(
        "bar",
        foo);
}

private string Temp(string value) =>
    value;

Interesting! We get a Func<string, string> instead of a simple Func<string>.

Inline everything

Just like before, inline Temp, temp and foo. We get:

[Fact]
void variable_is_replaced_with_method()
{
    Assert.Equal(
        "bar",
        
        ((Func<string, string>)(value =>
            value))
        ("bar"));
}

As is turned out, we get ("bar") instead of ().

In short

That’s basically it. Any:

VariableType variable = some_value;

{
  [body_using_variable]
}

can be replaced with

Func<VariableType, BodyType>(variable =>
  [body_using_variable]
)
(some_value)

where the original variable name is replaced with an anonymous lambda parameter, and its value with the argument the lambda is fed with.

Notice that the body of the Func in

[Fact]
Assert.Equal(
    "bar",
    ((Func<string, string>)(value =>
        value))
    ("bar"));

is the identity function value => value: this is because we converted the constant function.

Let’s go one level up, starting from a more complex function and using more variables.

Multiple variables

Replacing multiple variables with a lambda is equally mechanical. Let’s use the initial sample snippet:

[Fact]
void variables_are_used()
{
    var amount = 7;
    var product = "beers";
    var price = 3;

    Assert.Equal(
        "7 beers cost 21 EUR",
        $"{amount} {product} cost {amount * price} EUR");
}

Isolate the body

Apply Extract Method to the body, that is to $"{amount} {product} cost {amount * price} EUR")

[Fact]
void variables_are_used()
{
    var amount = 7;
    var product = "beers";
    var price = 3;

    Assert.Equal(
        "7 beers cost 21 EUR",
        Temp(amount, product, price));
}

private static string Temp(int amount, string product, int price) =>
    $"{amount} {product} cost {amount * price} EUR";

Create the Func

Convert Temp (without its arguments) to an anonymous lambda, with Introduce Variable:

[Fact]
void variables_are_used()
{
    var amount = 7;
    var product = "beers";
    var price = 3;

    Func<int, string, int, string> temp = Temp;
            
    Assert.Equal(
        "7 beers cost 21 EUR",
        temp(amount, product, price));
}

private static string Temp(int amount, string product, int price) =>
    $"{amount} {product} cost {amount * price} EUR";

Notice how type we get, Func<int, string, int, string>, it’s basically Func<type_var_1, type_var_2, type_var_3, type_body>`

Inline

Inline Temp with Inline Method

[Fact]
void variables_are_used()
{
    var amount = 7;
    var product = "beers";
    var price = 3;

    Func<int,string,int,string> temp = (amount, product, price) => $"{amount} {product} cost {amount * price} EUR";
            
    Assert.Equal(
        "7 beers cost 21 EUR",
        temp(amount, product, price));
}

Get rid of the variables

Apply Inline Variable to to get rid of all the variables:

[Fact]
void variables_are_used()
{
    Func<int,string,int,string> temp = 
        (amount, product, price) => 
            $"{amount} {product} cost {amount * price} EUR";
            
    Assert.Equal(
        "7 beers cost 21 EUR",
        temp(7, "beers", 3));
}

Inline the rest

Finally, inline temp:

[Fact]
void variables_are_used()
{
    Assert.Equal(
        "7 beers cost 21 EUR",
        ((Func<int, string, int, string>)(
            (amount, product, price) =>
                $"{amount} {product} cost {amount * price} EUR"))
        (7, "beers", 3));
}

Conclusion

Is that an end in itself?
Just a bit, but not quite. This is indeed one of the moves for replacing procedural code with functional expressions.

The equivalence between let expressions and lambdas is very well known in the Lisp’s circles. It’s not in C#’s, but sooner or later the experienced Functional Programmer will bump into it.

So what’s next? Go and use it for deriving Y. Have fun!

Comments

GitHub Discussions

References