DEV Community

Titouan CREACH
Titouan CREACH

Posted on

After one year of using CSharpFunctionalExtension

This article is not meant to tell you what you should do but it's a story of my journey of using CSharpFunctionalExtension as a lead dev, with a small team, not confortable with functional programming in general.

So why using CSharpFunctionalExtension ?

The main reasons that decided me to use CSharpFunctionalExtension were:

  • Error as value
  • Optional values (replacing nullable)
  • My background with functional programming
  • Pragmatism

Error as value

Error are simply object representing what happened. From a function, your return a Success representing the value, or an Failure representing an error. So you can't forget to handle the error.
In C#, nothing force you to catch an exception. You also don't know if the function you call will throw an exception and what kind of exception you may catch.
With error as value, you can see everything in the function declaration.

Optional values

Nullable is bad to handle the case of "optional" value. See: the billion dollar mistake for example.
For my case, the nullable flag was not enabled in our projects and it's impossible to tell if a object could be null or not. And it's so easy to forget.

Maybe<T> also give you a good documentation:

public Task<int> CreateUser(string firstname, string lastname, string email)
{ ... }
Enter fullscreen mode Exit fullscreen mode

vs

public Task<Result<int>> CreateUser(Maybe<string> firstname, Maybe<string> lastname, string email)
{ ... }
Enter fullscreen mode Exit fullscreen mode

I love when a function signature describe perfectly the intent of the function.

My background with functional programming

I did some OCaml, Elixir and Haskell as a hobby. I think I understand why the concepts like Maybe or Result are useful and elegant. It's frustrating to me when I see a situation where they would have perfectly fit but I can't use it.

Pragmatism

I've tested several functional programming librairies for C# and I found CSharpFunctionProgramming to be pragmatic and very well designed for my use cases.
I would like to be able to use procedural programming or even OO when I need to. I don't want to much ceremony and I would like my code to stay in the C# standard.
I would like a new hired people to understand the code.

So for that, I don't want too much purity. For example, I don't want IO Monads.

Tips I would love to have before I started

Don't scare your team with functional programming concepts

I noticed that starting to explain what a Functoror a Monad is was a mistake. Because my team directly put the concept in a trash and said: "too complicated for me". So let's start to explain a Maybe represents an optional value, and a Result, a result of an operation, either an error or a success.

So basically, you can check if a Maybe has a value with .HasValue property. And if yes, you can access .Value.
Ok, we know this is not sexy, but this is the starting point.

Be pragmatic

My second mistake was making things complicated. I was liking one-lining everything in a single expression, and chain operations. This resulted and complex code, hard to maintain. C# has no syntactic sugar for this (like do expression in Haskell).

After one year of experience with the lib, I can tell the most readable is early return.

Select functions to teach

I also have noticed there is some function that fit well in some situation. Let have a look to a subset of function I use everyday and where are the situation I would use them.

.TryGetValue
.Deconstruct
.GetValueOrDefault
.Map


.TryGetValue

I think the most used function (and the one I discovered very late) in my codebase is: TryGetValue.

TryGetValue allows to test if a Maybe has a value, and bind this value to a variable, in a single operation.

if (maybeEmail.TryGetValue(out var email))
{
    _email.SendTo(email);
}
Enter fullscreen mode Exit fullscreen mode

This function can also be used with Results and be combined with early return. The var you defined with out var ... is accessible in the enclosing scope, this allow you to do:

if (!emailSendingOperationResult.TryGetValue(out var recipientCount))
{
   // handle error here
   return whatever;
}

// use recipientCount here
Enter fullscreen mode Exit fullscreen mode

Deconstruct

The Deconstruct function is not used directly but implicitely used in a switch expression or a if statement.

A Maybe<T> is deconstructed into a tuple where the first element is .HasValue and the second one is value

var canDrinkBeers = maybeUser switch 
{
  (true, { Age: >=18, Region: "EU" }) => true,
  (true, { Age: >=21 }) => true,
  _ => false
};
Enter fullscreen mode Exit fullscreen mode

I use deconstruct to mix the Maybe usage and the native C# pattern matching (guard etc)


GetValueOrDefault()

The classic: .GetValueOrDefault(), returns the default of the underlying Maybe type, or the first argument you pass to it.

var cpuCount = configurationMaxCpu.GetValueOrDefault(4); 
Enter fullscreen mode Exit fullscreen mode

Map

The Map function is extremely useful but, I find myself not using it too much. Depends on the case. I often use it when my "mapping function" takes no argument.


HandleMessage(
    maybeMessage.Map(Encoding.UTF8.GetString)
)

Enter fullscreen mode Exit fullscreen mode

Spend time to do code review

Some months after the switch, when I was more confident about how should we code, I tried to spend time in code review. Remember functional programming is not that intuitive at first.

Real code example from a junior dev:


Maybe<int> paymentMethodId = Maybe<int>.None;

if (paymentMethod.HasValue)
{
    paymentMethodId = paymentMethod.Value.Id;
}

var checkoutResult = await _checkoutService.CreateCheckout(paymentMethodId);
Enter fullscreen mode Exit fullscreen mode

Just tell there is a function that fit exactly for this need, the map function:

var checkoutResult = await _checkoutService.CreateCheckout(
  paymentMethod.Map(x => x.Id)
);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)