DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Zen and Functional C#
Henrick Tissink
Henrick Tissink

Posted on • Updated on

Zen and Functional C#

Writing functional programming can induce a Zen like experience. With clean, short blocks of code that reveal their intent and inner workings plainly to you functional C# untangles the knots, and declutters the space.

No more side effects

Take a deep breath in. Hold it for 5 seconds. Exhale.

It's time to think functionally, and it's time to say goodbye to those nasty, no-good side effects.

alt text

  • Side effects occur when you modify state outside of your current scope, mutating state unknowingly somewhere else in the program. This leads to unwanted behaviour, and unwanted cause-and-effects.
  • Instead prefer functions that don't have side effects. These functions are known as pure functions. (disclaimer: writing pure functions is quite addictive - once you start you just can't stop)

The Zen Code Advises:

AVOID: Impure functions

public void UpdatePrice(decimal vat, decimal cost, decimal markup)
{
    this.price = (cost + markup) * (1 + vat);
}
Enter fullscreen mode Exit fullscreen mode

PREFER: Pure functions

public decimal PriceIncludingVat(decimal vat, decimal cost, decimal markup)
{
    return (cost + markup) * (1 + vat);
}

price = PriceIncludingVat(0.2m, 10m, 2m);
Enter fullscreen mode Exit fullscreen mode

Calling PriceIncludingVat doesn't modify existing state; it returns new state. You can call it as many times as you want and your current price won't be affected until you actually set it.

Practice creating pure functions out of longer impure methods. Break the side effects out of methods and turn them into their own functions returning new state. You may find yourself untangling some really, nasty messes - it's cathartic and brings you closer to Zen.

Immutability

Close your eyes. Breathe in. Breath out. Embrace the tranquillity of immutability.

alt text

No longer will your state be mutable. In with the new, and out with the old. By making your objects immutable, you ensure that nothing, within any scope whatsoever, can change them. This prevents side effects altogether. The only way to achieve a changed property on an immutable object is to replace that object with a new one that contains the change.

The Zen Code Advises:

AVOID: Mutable Objects

public class ZenGarden
{
    public int Tranquillity { get; set; } // mutable
    public int Peace { get; set; } // mutable

    public ZenGarden(int tranquillity, int peace)
    {
        Tranquillity = tranquillity;
        Peace = peace;
    }
}
Enter fullscreen mode Exit fullscreen mode

This leads to

var garden = new ZenGarden(10, 20);
garden.Peace = -1000;
// or even worse
public int TranquilityAndPeace(ZenGarden garden)
{
    garden.Peace -= 1; // a side effect
    return garden.Tranquillity + garden.Peace;
}
Enter fullscreen mode Exit fullscreen mode

PREFER:

public class ZenGarden
{
    public readonly int Tranquillity;
    public readonly int Peace;

    public ZenGarden(int tranquillity, int peace)
    {
        Tranquillity = tranquillity;
        Peace = peace;
    }
}
Enter fullscreen mode Exit fullscreen mode

This leads to

var garden = new ZenGarden(10, 20);
garden = new ZenGarden(10, 21); // peace goes up, because of immutability :]
// pure function
public int TranquilityAndPeace(ZenGarden garden)
{
    var calculatedGarden = new ZenGarden(garden.Tranquillity, garden.Peace - 1);
    return calculatedGarden .Tranquillity + calculatedGarden .Peace;
}
Enter fullscreen mode Exit fullscreen mode
  • Note, readonly is used instead of a private setter to ensure that the values can't be changed even within the class itself.

No more worries about unwanted changes (side effects) - immutability ensures that nothing is mutating the same state, wave a friendly goodbye to race conditions and thread locks too.

Referential Transparency

Embrace the Zen of clarity. A referentially transparent function can be replaced by the value that it returns when it's called. It returns the same value for the same arguments regardless of its context. Referentially opaque functions do the opposite - usually due to referencing some external value which may change.

Referential transparency helps the code to read cleaner. It untangles functions that grab values from outside of their scope, and restricts them to act only on their parameters when calculating their return values.

It isolates functions, protecting them from external changes in code.

alt text

The Zen Code Advises:

AVOID: Opaque functions

public double PriceOfFood(double price) 
{
    // if the number of people changes, the price of food changes
    // regardless of price changing
    return this.numberOfPeople * price;
}
Enter fullscreen mode Exit fullscreen mode

PREFER: Transparent functions

public double PriceOfFood(double price, int numberOfPeople) 
{
    return numberOfPeople * price;
}
Enter fullscreen mode Exit fullscreen mode

Higher-Order Functions

It's first-class (functions) baby all the way. Let's pick those functions up and start treating them like the (first-class) citizens they are. C# treats functions like first-class citizens. This means that functions can be used as arguments to methods; functions can be assigned to variables; and function can even be returned from other functions.

Using functions as first-class citizens allows us to break our code into small building blocks, each of which encapsulates a little bit of (hopefully clean, pure, and referentially transparent) functionality. These small blocks can be used in many combinations to get different functionality. They can be swapped out and interchanged as our requirements change with little impact on the code base.

alt text

Generate your functionality as you need it - don't write reams of code. Let the functions behave like the moving parts we know they are.

Higher-order functions take in functions as parameters, or return functions. Because C# treats functions as first-class citizens it allows as to create higher-order functions.

The Zen Code Advises:

AVOID: Unclear lambdas

var range = Enumerable.Range(-20, 20);
range.Where(i => i % 2 == 0);
range.Where(i => i % 5 != 0);
Enter fullscreen mode Exit fullscreen mode

PREFER: Named functions

Func<int, bool> isMod(int n) => i => i % n == 0;
range.Where(isMod(true, 2));
range.Where(!isMod(false, 5));
Enter fullscreen mode Exit fullscreen mode

AVOID: Procedural functional calls (where applicable)

public class Car {...}

public void ChangeOil(Car car) {...}
public void ChangeTyres(Car car) {...}
public void Refuel(Car car) {...}

public void PitStop(Car car)
{
    ChangeOil(car);
    ChangeTyres(car);
    Refuel(car);
}

PitStop(car);
Enter fullscreen mode Exit fullscreen mode

PREFER: Dynamic function calls (where applicable)

public class Car {...}

public void ChangeOil(Car car) {...}
public void ChangeTyres(Car car) {...}
public void Refuel(Car car) {...}

public void PitStop(Car car, IEnumerable<Action<Car>> pitstopRoutines)
{
    foreach(var routine in pitstopRoutines)
        routine(car);
}

PitStop(car, new List<Action<Car>>{ChangeOil, ChangeTypes, Refuel});
Enter fullscreen mode Exit fullscreen mode

The method PitStop is much more extensible. It can easily be extended and should hardly ever need modifying. This is achieved by simply passing in the functions you want to execute as parameters, treating them like first-class citizens.

Now go forth and embrace the Zen my dudes

alt text

Top comments (9)

Collapse
 
ferdeen profile image
Ferdeen Mughal

Hi Henrick, really good article, I'm going to start putting this into practice!

Also noticed the sample code 'public void PriceOfFood...' should return double.

Cheers

Collapse
 
htissink profile image
Henrick Tissink

Hi Ferdeen, I really appreciate the positive feedback, thank you so much!

And well spotted - fixed it :]

Collapse
 
ferdeen profile image
Ferdeen Mughal

No problem!

Was checking where I commented on the typo and could see a couple more void returns that should also return double. They are towards the top of the post.

Cheers!

Thread Thread
 
htissink profile image
Henrick Tissink

Oh jeez - I checked the writing for typos but not the code - thanks again :]

Thread Thread
 
ferdeen profile image
Ferdeen Mughal

No worries. It doesn't take anything away from such a good post, and I'll look forward to reading more of your work. Cheers.

Collapse
 
tyronasaurus_dev profile image
Tyron Barlow-Kearsley • Edited on

For a janitor, you really know a lot. You're like the Will Hunting of programming

willhunting

Collapse
 
htissink profile image
Henrick Tissink

Hahaha, thanks :) alt text

Collapse
 
jimmydeemo profile image
JimmyDeemo

Love this. I get jaded about the language I code in professionally; sees C# is steeped in methodolody that is counterintuitive to what is being achieved.

Collapse
 
htissink profile image
Henrick Tissink

Thanks Jimmy - yes exactly. The .Net team have added so much functionality to C# that you don't always know how to use it sometimes. Sticking to a clean functional approach helps; and it's a lot of fun switching your thinking from procedural to functional.

DEV

Thank you.

Β 
Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.