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)
{ ... }
vs
public Task<Result<int>> CreateUser(Maybe<string> firstname, Maybe<string> lastname, string email)
{ ... }
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 Functor
or 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);
}
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
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
};
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);
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)
)
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);
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)
);