Functional programming

iamprovidence
9 min readMar 17, 2024

I know what you thinking. You read the heading and be like “Eww.. I am a C# developer. I am not sticking my hands into that 🤢”.

Hear me out. Even if you an adherent of object-oriented programming, it is important to draw wisdom from many different places. If we take it only from one place, it becomes rigid and stale. Understanding other paradigms will help you to become a highly qualified specialist.

In this article, you will see what Functional programming is about. Delve into the concept of pure functions and how to write those. The new way to deal with exceptions. You will see that primitive obsession is discouraged even in functional programming. You will discover why functional programming is so inseparable from immutability. For the final, you will be able to apply and get used of it in practice even if you are a C# developer.

I hope that was enough to drag you into. Let’s begin, shall we 😉?

Functional programming

Functional programming is a programming paradigm that enforces writing pure functions.

For a function to be “pure” it should satisfy the following conditions:

  • be deterministic
  • has no side effects

That is it 😁. I told you, there is nothing to worry about 😂.

In fact, despite its simple definition, functional programming requires a lot of skills to master it. Let’s see some challenges and how those can be overcome.

Determinisity

A function is considered deterministic if, given the same input, it will always produce the same output.

Have a look at the example below:

static class Math 
{
private static int _sum = 0;

public static int Add(int a)
{
_sum += a;
return _sum;
}
}

By calling the function multiple times with the same arguments it will return different results every time.

Math.Add(1); // 1
Math.Add(1); // 2

The function depends on an external state outside of its scope. The behavior of the function cannot be forecasted. The result may be different depending on the order, number of calls, where it is called, at which time, etc. It will most definitely lead to bugs and harder code maintenance.

Compare it to this example:

static class Math 
{
public static int Add(int a, int b)
{
return a + b;
}
}

This time we don’t depend on a state, only on provided arguments. If those are the same, we can always expect the same behavior:

Math.Add(1, 1); // 2
Math.Add(1, 1); // 2

To achieve determinisity, a function should not depend on anything external.

Side effects

A function has no side effects if there is no observable change that a function makes outside of its own scope.

Sounds over academically, right?👨‍🎓 There is also an easier explanation.

A function has no side effects if it does nothing unexpected.

Take a look at the next example:

static class Math 
{
public static int Multiply(int a, int b)
{
var res = a * b;
File.AppendAllText("result.txt", res);
return res;
}
}

The scope of the function is pretty narrow, it is just two arguments. However, for some unknown reasons, it also writes to an external file. The caller of this could definitely not expect that.

The cleaner version would be next:

static class Math 
{
public static int Multiply(int a, int b)
{
return a * b;
}
}

To eliminate side effects, a function should not affect anything external.

Exceptions

It may not be so clear, but the example below also violates the “no side effect” rule:

static class Math 
{
public static int Divide(int a, int b)
{
return a / b;
}
}

If you call it this way:

Math.Divide(2, 0);

It will throw DivideByZeroException.

The question asks itself. Why this function has a side effect?:
a) it does not affect anything outside its scope
b) the result is expected

The answer is as follows:
a) it does affect the scope of the caller code. The execution flow will be interrupted and passed above to the first corresponding catch block
b) the exception is indeed unexpected. Maybe not for you, since you know C#, since you know math, since this example is simple. However, nothing from the method signature guides us about the exception

Instead of a simple Divide() we could have something like this:

User user = _userService.CreateNewUser("John", null);

How can you be sure it won’t throw any exception? You need to dive into the implementation of the method and investigate all its possible execution flows, to see if any of those does not end up in the exceptional state. What if the method calls other methods? You need to investigate those as well down to the very end.

Result class

To get rid of side effects we need to get rid of exceptions.

One of the possible ways would be to change the returned type.

Something simple like changing int to int? will do the work.

static class Math 
{
public static int? Divide(int a, int b)
{
if (b == 0) return null;

return a / b;
}
}

By doing so, the caller code will know that the result may not exist and is forced to handle this case.

var result = Math.Divide(2, 0);

if (result.HasValue)
{
// do something with a value
]
else
{
// I am pretty much sure you will figure something out 😉
}

In more complex scenarios you can consider a tuple:

static class Math 
{
public static (bool success, int result) Divide(int a, int b)
{
if (b == 0) return (false, default);

return (true, a / b);
}
}

However, the most flexibility provides a Result class:

class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public IEnumerable<ErorrCode> Errors { get; }

. . .
}

It can be extended the way you like. Add other properties based on your needs. Consider having a static factory method with clear names for readability; type conversion operators for purity etc.

static class Math 
{
public static Result<int> Divide(int a, int b)
{
if (b == 0) return Result.Error(ErorrCode.CannotDivideByZero);

return Result.Success(a / b);
}
}

Primitive Obsession

The other way to get rid of exceptions would be to change the input arguments type.

We can have positiveInt instead of int for the second argument:

static class Math 
{
public static int Divide(int a, positiveInt b)
{
return a / b;
}
}

This way we make sure the function won’t be called with unexpected arguments.

Math.Divide(2, 0); // compile-time error

The problem here C# does not have an in-build type that represents only positive integers starting from one😔.

Unexperienced or just lazy developers would give up and leave stuff as is. This is the cause of a known issue called primitive obsession.

Primitive obsession refers to the tendency of using primitive data types for representing domain concepts instead of creating dedicated domain-specific types.

Same here. If there is no in-build type to help us, we can create are own type:

class NonZeroInt
{
public int Value { get; }

public static Result<NonZeroInt> Create(int value)
{
if (value <= 0) return Result.Error(ErorrCode.ValueIsLessThanOrEqualZero);

return new NonZeroInt
{
Value = value,
};
}
. . .
}

A custom type brings better encapsulation, clarity, and type-safety to the code. It also reminds us good old OOP, that we, C# developers, are so obsessed with 😏.

Immutability

Custom objects do not necessarily guard us from side effects:

static class Math 
{
public static int Divide(int a, NonZeroInt b)
{
b.Value = 0; // affect caller scope

return a / b;
}
}

If there were a set on Value property we would mutate it. This would affect the caller’s scope and cause unexpected behavior. Gladly NonZeroInt is an immutable type.

Immutability refers to the inability of an object or data structure to be modified after it is created. Once created, an immutable object cannot be changed, and any operation on the object returns a new object with the desired modifications.

There are some benefits for an object to be immutable:

  • predictability. a fixed state, makes it easier to reason about the behavior
  • concurrency. since immutable objects cannot be modified after creation, they are inherently thread-safe. This reduces the risk of race conditions in concurrent programming
  • debugging. immutability makes it easier to identify when and where data is modified since any change results in the creation of a new object. This aids in debugging and maintaining a clear code history
  • caching. immutable objects can be safely cached, as their values never change

However, there is always a cost:

  • memory overhead. creating new objects for every modification can lead to increased memory usage. This may impact performance in situations where memory is a critical resource
  • complexity. embracing immutability might require a mindset shift for developers accustomed to mutable structures. Working effectively with immutable patterns can be challenging

Simple data types like int, decimal, enum, string, etc are immutable by their nature.

With a method like this, we can make sure it won’t modify input arguments:

public int Sum(int a, int b, int c) { . . . }

Unless ref is used. This is why ref and out keywords are unwelcomed while in is encouraged. Indeed immutability means not only an immutable state but the prohibitance of reassigning.

Imagine something like that in the code:

public int Sum(int a, int b, int c) 
{
a = 4;
. . .
}

It may not affect the outer scope nor any side effects are created, however, it still can cause a lot of bottom pain for a developer who missed that line in the ocean of a legacy code.

Once you are assigned, you never get reasigned🧙‍♂️ . JS developers understood the advantages of it a while ago. They use const instead of let all over the place.

Even if you don’t reassign anything, you still can mutate input arguments. Consider the next method:

public int Sum(List<int> numbers) 
{
numbers.Add(4);
. . .
}

List<int> is mutable, and the Sum() method can add more numbers to the collection causing side effects for the calling code. This can be avoided with the use of an immutable interface:

public int Sum(IReadOnlyList<int> numbers) { . . . }

Things get more complicated with objects. Let’s say we have the next class:

class User
{
public int Name { get; }

public void ChangeName(string newName)
{
Name = newName;
}
}

Even though it does not have a public set, we need to investigate all class behaviors, to make sure none of the methods mutate its state. Ideally, ChangeName() should return a new User object.

With a graph of objects, it is even more complicated. Any changes to the nested object will cause the recreation of the entire graph:

class User
{
public List<User> Followers { get; }
. . .
}

Even though, C# is not intended for immutable models, there are a lot of facilities to achieve it: readonly structs, records, readonly collections, immutable collections, etc.

C# developer’s thoughts

Up to this point, I tried to give you my unbiased opinion on the topic. However, now I want to share some of my personal experiences about applying functional programming.

I think anybody can benefit from writing pure functions. Determinisity and absence of side effects make your code robust and easy to maintain.

Although not using exceptions feels uncommon and goes against what we always have been learning, in practice, you quickly get used to it. Especially when your entire application is designed this way. Exceptions are not as horrifying as they sound. Just be consistent and stick to the same approach everywhere. I will give the Result class a big thumbs up.

Even though avoiding primitive obsession sounds great, most of the time I find myself using a string with some regex over Email or Url class 😬 . Yes, I have this sin, I repent.

The only challenge was immutability. It suits perfectly for DTOs, value objects, input/output arguments, etc. However, for entities, that our distinguished by their IDs, probably tracked with EF, passed across multiple function calls, it creates more complexity than helps.

I still define my domain objects with mutable behavior. Not doing so, just seems to me like protection from dummy developers. Rules of clean code and feel of common sense will protect you from making mistakes. In case they don’t, and your colleagues are true dummies, you can always passive-aggressively point it out during the PR review process🙃

Conclusion

Functional programming is a programming paradigm that teaches us how to write functions.

It avoids changing state and mutable data. Functional programming helps to deal with complexity by prohibiting reassignment operations.

Despite obvious benefits, there are a few hidden ones:

  • memoization. since functions are deterministic you can cache a result for the same input arguments
  • thread-safty. having no dependencies between arguments, you can have functions called in arbitrary order or in different threads
  • reusability. functions that don’t rely on the external state are more reusable, promoting modular and composable code
  • referential transparency. pure function can be replaced with its value
  • unit tests. functions that get all its state from outer code easier to mock and cover with unit tests

That is all I had for you today folks. 🙃

💬 Let me know in the comments what is your experience with functional programming. Have you tried it? Did you like it?

👏 Clap for this article if you enjoyed

👇 If you don’t like this one, there are other articles you may want to check out

☕️ You can also buy me a coffee with a link below

✅ Follow not to miss out more

😉 But most importantly, stay awesome

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence