Examples
Option
The Option type represents the concept that an item may or may not exist. In some ways C# already does this with the idea of the null value. However, in previous .NET and C# versions, objects could still be null even if their type signature indicated that they were a real, valid object. External packages may still have this issue, but internally you can force your code to obey the nullable rules using the Nullable directive in .NET 6 and later.
ScratchPad.csproj<Nullable>enable</Nullable>
The following example shows how previous versions of .NET and C# would allow possible null returns. Using FirstOrDefault can produce a null value.
| Valid or null...? |
|---|
| using System;
using System.Collections.Generic;
using System.Linq;
namespace ScratchPad;
public record TodoItem(string Title, bool Completed);
public static class Program
{
public static List<TodoItem> TodoItems =>
new()
{
new("Use CSharp.Made.Functional", false),
new("Read the Docs", true)
};
// Hides its true return type which should be 'TodoItem?'
public static TodoItem FindTodoItem(string title)
{
var todoItem =
TodoItems
.Where(todo => todo.Title.Contains(title))
.FirstOrDefault();
return todoItem;
}
public static void Main()
{
TodoItem todoItem = FindTodoItem("Title Which Doesn't Exist");
// What does this print?
Console.WriteLine(todoItem.Title);
}
}
|
Optional
What if we could make the return type more explicit, beyond just changing the return type to TodoItem? for the FindTodoItem method? Shown in the example below, the Option type helps to declare that the return type might not exist. We can use the Optional extension to wrap values which may be null. When a value does not exist, it is represented by None. If a value exists, it is represented by Some.
There are ways to get the internal value of the Option and those will be described in later examples. Looking at the previous example again, let's use the Optional extension method to improve the function signature.
| Program.cs |
|---|
| using System.Collections.Generic;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record TodoItem(string Title, bool Completed);
public static class Program
{
public static List<TodoItem> TodoItems =>
new()
{
new("Use CSharp.Made.Functional", false),
new("Read the Docs", true)
};
// Honest function signature which indicates optional value.
public static Option<TodoItem> TryFindTodoItem(string title) =>
TodoItems
.Where(todo => todo.Title.Contains(title))
.FirstOrDefault()
.Optional();
public static void Main()
{
Option<TodoItem> todoItem = TryFindTodoItem("Title Which Doesn't Exist");
// Ok, but how do we print the title now?
}
}
|
Some and None
To create a Some or None directly, without using the Optional extension, the Prelude class provides helpers for this.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public static class Program
{
public static void Main()
{
var someMessage =
// Type is inferred to be string.
Some("Hello, world")
.Reduce("Won't be me!");
var noneMessage =
// Type can't be inferred, so one must be provided.
None<string>()
.Reduce("It was None");
// Prints "Hello, world"
Console.WriteLine(someMessage);
// Prints "It was None"
Console.WriteLine(noneMessage);
}
}
|
Map, Filter, and Reduce
In the example showing the Optional extension, we improved the function
signature of the TryFindTodoItem method. Let's go a step further and find
out how we can actually get the value of out an Option type using
Map, Filter, and Reduce. The Map method performs mapping on the
internal type that is wrapped by the Option. In the example, our internal
type is a TodoItem. Below we use it to get the title using Map in the case
that the Option is a Some. How do we represent a title if the Option is None?
This is where Reduce comes in. We can supply an alternate value directly,
or we can supply a function that returns an alternate value.
The function approach is useful in cases where getting an alternate value
might be computationally expensive. Finally, Filter is used to convert a
Some to a None when the filter criteria evaluates to true.
| Program.cs |
|---|
| using System;
using System.Collections.Generic;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record TodoItem(string Title, bool Completed);
public static class Program
{
public static List<TodoItem> TodoItems =>
new()
{
new("Use CSharp.Made.Functional", false),
new("Read the Docs", true)
};
public static Option<TodoItem> TryFindTodoItem(string title) =>
TodoItems
.Where(todo => todo.Title.Contains(title))
.FirstOrDefault()
.Optional();
public static void Main()
{
string readTheDocs =
TryFindTodoItem("Read")
.Map(todoItem => todoItem.Title)
.Reduce("Not Found");
string notFound =
TryFindTodoItem("Read")
.Filter(todoItem => todoItem.Completed == false)
.Map(todoItem => todoItem.Title)
.Reduce(() => "Not Found");
// Prints "Read the Docs" because the item exists.
Console.WriteLine(readTheDocs);
// Prints "Not Found" because the TodoItem is Completed, so it is filtered out.
Console.WriteLine(notFound);
}
}
|
Option Match
Sometimes Map and Reduce may not feel like the right solution to a problem.
In those cases it may feel more natural to reach for the Match method.
Match expects two mapping functions to be provided to extract the value
of the Option. See below for an alternate approach using Match.
| Program.cs |
|---|
| using System;
using System.Collections.Generic;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record TodoItem(string Title, bool Completed);
public static class Program
{
public static List<TodoItem> TodoItems =>
new()
{
new("Use CSharp.Made.Functional", false),
new("Read the Docs", true)
};
public static Option<TodoItem> TryFindTodoItem(string title) =>
TodoItems
.Where(todo => todo.Title.Contains(title))
.FirstOrDefault()
.Optional();
public static void Main()
{
string readTheDocs =
TryFindTodoItem("Read")
.Match(
todoItem => todoItem.Title,
() => "Not Found");
string notFound =
TryFindTodoItem("Read")
.Filter(todoItem => todoItem.Completed == false)
.Match(
todoItem => todoItem.Title,
() => "Not Found");
// Prints "Read the Docs" because the item exists.
Console.WriteLine(readTheDocs);
// Prints "Not Found" because the TodoItem is Completed, so it is filtered out.
Console.WriteLine(notFound);
}
}
|
Option Bind
Since the Map function performs an action on an Option only when it
is Some, the return type of Map is still Option. This means that
inside the mapping operation, we usually want to do transformations
that don't result in another Option type, otherwise we would end up
with Option<Option<T>>. This is the reason for the Bind function.
We can use Bind instead of Map.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public static class Program
{
// This could be some database call which may result in None
public static Option<string> TryFindString(string input) =>
Some(input);
// Trivial example, but shows how mapping to an option requires Bind.
public static Option<string> NoEmptyStringsAllowed(string input) =>
string.IsNullOrWhiteSpace(input) switch
{
true => None<string>(),
false => Some(input),
};
public static void Main()
{
var toPrint =
TryFindString("Hello, World!")
.Bind(NoEmptyStringsAllowed)
.Reduce("Not Found");
// Prints "Hello, World!"
Console.WriteLine(toPrint);
}
}
|
Option Effect
If we just want to perform some Action which is somewhat of a side-effect,
we can use the Effect method. Using the previous example, we can
significantly simplify the code if we just want to do a single
Console.WriteLine. We also don't have to print anything if the
TodoItem is None.
| Program.cs |
|---|
| using System;
using System.Collections.Generic;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record TodoItem(string Title, bool Completed);
public static class Program
{
public static List<TodoItem> TodoItems =>
new()
{
new("Use CSharp.Made.Functional", false),
new("Read the Docs", true)
};
public static Option<TodoItem> TryFindTodoItem(string title) =>
TodoItems
.Where(todo => todo.Title.Contains(title))
.FirstOrDefault()
.Optional();
public static void Main() =>
TryFindTodoItem("Read")
.Effect(
todoItem => Console.WriteLine(todoItem.Title),
() => { /* We don't have to print anything actually... */ });
}
|
Option EffectSome and EffectNone
If we only want to perform an effect when an Option is Some or
when it's None, we can use the EffectSome and EffectNone methods
which will consume the Option.
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using static Functional.Prelude;
using System;
public static class Program
{
public static void Main()
{
// This will print "value" to the console.
Some("value")
.EffectSome(value => Console.WriteLine(value));
// This will do nothing since the input value was a None.
None<string>()
.EffectSome(value => Console.WriteLine(value));
// This will do nothing since the input is a Some.
Some("value")
.EffectNone(() => Console.WriteLine("won't print"));
// This will print "no value" since the input was None.
None<string>()
.EffectNone(() => Console.WriteLine("no value"));
}
}
|
Option Tap, TapSome and TapNone
If we want to Tap into the Option and perform some effect without
consuming the value, we can use Tap, TapSome, and TapNone.
With Tap, some kind of action must be provided for both
the Some and None cases.
For TapSome and TapNone, one or more actions can be provided
to occur when the Option meets that criteria. This will allow us
to only perform actions when the value is Some or None for instance.
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using static Functional.Prelude;
using System;
public static class Program
{
public static void Main()
{
// Actions can be performed when some and none.
Some("value")
.Tap(some => Console.WriteLine(some), () => Console.WriteLine("none"))
// The Option is not consumed so we can still use it afterwards.
.Map(some => some + "!");
// We can do multiple things when the value is Some with a TapSome.
string? temp = null;
Some("value")
.TapSome(
value => Console.WriteLine(value),
value => temp = value);
// Nothing happens here since the value is a None.
None<string>()
.TapSome(value => Console.WriteLine(value));
}
}
|
Option Unwrap
If we need to get the value out of an Option for some reason and it's
impractical to use Match, Map, Tap, or Effect, we can Unwrap the
value to get its inner contents.
It's vital to check to see if the Option is Some before doing this,
otherwise it will throw an exception!
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using static Functional.Prelude;
using System;
public static class Program
{
public static void Main()
{
// This will unwrap fine because the value is some.
string value = Some("value").Unwrap();
// This will throw an exception because the value is none.
string never = None<string>().Unwrap();
// To do this safely, we need to always check the Option first!
var option = None<string>();
if (option.IsSome)
{
value = option.Unwrap();
}
}
}
|
Async Options
Asynchronous support is also provided in this library. The Optional extension
also works on Task<T> where T is some type. This means that Task<T>
or Task<T?> becomes Task<Option<T>>.
Async Option Methods:
FilterAsync
MapAsync
ReduceAsync
MatchAsync
BindAsync
EffectAsync
EffectSomeAsync
EffectNoneAsync
TapAsync
TapSomeAsync
TapNoneAsync
UnwrapAsync
UnwrapErrorAsync
| Program.cs |
|---|
| using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record TodoItem(string Title, bool Completed);
public static class Program
{
public static List<TodoItem> TodoItems =>
new()
{
new("Use CSharp.Made.Functional", false),
new("Read the Docs", true)
};
// AsAsync is an extension method to wrap an item with a Task.
public static async Task<Option<TodoItem>> TryFindTodoItem(string title) =>
await TodoItems
.Where(todo => todo.Title.Contains(title))
.FirstOrDefault()
.Async()
.Optional();
public static async Task Main()
{
string readTheDocs =
await TryFindTodoItem("Read")
.MapAsync(todoItem => todoItem.Title)
.ReduceAsync("Not Found");
string notFound =
await TryFindTodoItem("Read")
.FilterAsync(todoItem => todoItem.Completed == false)
.MapAsync(todoItem => todoItem.Title)
.ReduceAsync("Not Found");
// Prints "Read the Docs" because the item exists.
Console.WriteLine(readTheDocs);
// Prints "Not Found" because the TodoItem is Completed, so it is filtered out.
Console.WriteLine(notFound);
}
}
|
Union
The Discriminated Union represents a type which may have multiple sub-types
called variants. This is a very powerful concept which can be used to model
many different real-world relationships in programming. Here is an example
of what this looks like in F#. Using the built-in match expression in F#,
we have a compile-time exhaustive match expression.
| Program.fs |
|---|
| type Animal =
| Cat
| Dog
| Bird
let GetMysteryAnimal () : Animal = Bird
let MakeAnimalNoises () : string =
match GetMysteryAnimal() with
| Cat -> "Meow"
| Dog -> "Ruff"
| Bird -> "Tweet"
|
In C#, this concept is often modeled using inheritance. This also means that
we can't predict every possible type which could inherit from the original
Animal class. So, the switch expression is as close to the F# match as
we can get. Unfortunately for us, we always have one last annoying discard case.
At best, we have a case which is never used, but is required for the code to compile.
At worst, we probably have undetected logic bugs. For instance, when
another variant is added as an Animal type, which should have a real
animal sound, it will return "What Goes Here" instead of a real value.
We are sadly never alerted by the compiler that we haven't handled the new
variant. Problematic, indeed.
| Program.cs |
|---|
| namespace ScratchPad;
public class Animal { }
public class Bird : Animal { }
public class Dog : Animal { }
public class Cat : Animal { }
public static class Program
{
public static Animal GetMysteryAnimal() =>
new Bird();
public static string MakeAnimalNoises() =>
GetMysteryAnimal() switch
{
Bird => "Tweet",
Dog => "Ruff",
Cat => "Meow",
_ => "What Goes Here?",
};
public static void Main() { }
}
|
Create your own Union
What if we could have compile-time type checking that ensured we handled every
variant? It is possible using the Union type. In fact, Option and Result
use the Union type underneath. In order to ensure strong compile-time variant
checking, there are different names for each Union type to handle different
numbers of variants. For example, Option uses the regular Union type because
it has two variants, Some and None. In our animal example, however, we have
3 variants that we care about. So in this case, we need to use Union3. The
following list shows the maximum number of variants that are currently supported
by this library. I would posit that if more variants are needed, it's possible
that another data modeling strategy may be a better fit. Remember, every variant
must be handled, so for Union9 this means that every match expression must
have 9 different functions in order to handle every possible case.
Currently supported Unions:
Union
Union3
Union4
Union5
Union6
Union7
Union8
Union9
This implementation is a lot more verbose than the F# version, which is why C#
needs built-in support for discriminated unions. However, it does accomplish
the same goal. It is recommended that you seal any Union classes that you
create in order to make sure that other classes cannot inherit from it.
There must be a public constructor for each variant and no other constructors.
It's also recommended that you implement the Match method as shown below
to access the internal Union contents and use the underlying Union.Match
method. It is also recommended to create static factory methods named after
each variant. This will simplify the way that Animal instances are created.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public sealed class Animal
{
private Union3<Bird, Dog, Cat> contents;
public Animal(Bird bird) => contents = new(bird);
public Animal(Cat cat) => contents = new(cat);
public Animal(Dog dog) => contents = new(dog);
public T Match<T>(Func<Bird, T> whenBird, Func<Dog, T> whenDog, Func<Cat, T> whenCat) =>
contents
.Match(whenBird, whenDog, whenCat);
public static Animal Cat() => new(new Cat());
public static Animal Dog() => new(new Dog());
public static Animal Bird() => new(new Bird());
}
public record Bird()
{
public string Tweet => "Tweet";
}
public record Cat()
{
public string Meow => "Meow";
}
public record Dog()
{
public string Bark => "Ruff";
}
public static class Program
{
public static Animal GetMysteryAnimal() =>
Animal.Bird();
public static string MakeAnimalNoises() =>
GetMysteryAnimal()
.Match(
bird => bird.Tweet,
dog => dog.Bark,
cat => cat.Meow);
public static void Main()
{
// Prints "Tweet"
Console.WriteLine(MakeAnimalNoises());
}
}
|
Errors instead of Exceptions
C# makes heavy use of exceptions, but not all behavior is truly exceptional.
For instance, if an item can't be found in a database, should we throw a
NotFoundException and cause the server to crash if it's not handled?
Instead, we can use the Union type to create an error type which can be used
to model errors that could happen during processing. Here is an example of
an error which could happen when performing database operations with an
Animal object. In a web application, instead of returning a string,
we could match the type of error and return an HttpResponse related to
that type of error as an example.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public sealed class AnimalError
{
private Union3<NotFound, Invalid, Unhandled> contents;
public AnimalError(NotFound notFound) => contents = new(notFound);
public AnimalError(Invalid invalid) => contents = new(invalid);
public AnimalError(Unhandled unhandled) => contents = new(unhandled);
public T Match<T>(Func<NotFound, T> notFound, Func<Invalid, T> invalid, Func<Unhandled, T> unhandled) =>
contents
.Match(notFound, invalid, unhandled);
public static AnimalError NotFound() => new(new NotFound());
public static AnimalError Invalid() => new(new Invalid());
public static AnimalError Unhandled() => new(new Unhandled());
}
// Like a 404
public record NotFound();
// Like a 400
public record Invalid();
// Like a 500
public record Unhandled();
public static class Program
{
public static AnimalError DatabaseOperationWhichErrors() =>
AnimalError.NotFound();
public static string CreateNewAnimal() =>
DatabaseOperationWhichErrors()
.Match(
notFound => "Animal was not found",
invalid => "The animal was invalid",
unhandled => "Something unexpected happened");
public static void Main()
{
// Prints "Animal was not found"
Console.WriteLine(CreateNewAnimal());
}
}
|
Built-in Methods
All of the Union variants have Effect and Match built-in. This means that
if you create a custom Union type, you can use the inner Union built-in
methods to expose public versions on your custom Union type. An example of
this is shown in the previous Animal example with the Match method.
Result
In the previous example, the database operation only ever returned an error.
But in real life, database operations would produce a good result sometimes,
and an error result other times. We could roughly categorize these as successes
and failures. In trying to keep with the same syntax as F#, this library calls
successes Ok and failures Error. This is what a Result looks like in F#.
| Program.fs |
|---|
| open System
type CustomError = { message: string }
let NoEmptyStrings (input: string) : Result<string, CustomError> =
input
|> String.IsNullOrWhiteSpace
|> function
| true -> Error { message = "Empty strings are not allowed." }
| false -> Ok input
|
If you're not accustomed to F#, try not to focus too much on the syntax,
just recognize that we can return either an Ok or an Error from the
function and that's perfectly fine. In the example you will notice a few
operators called Pipe which look like this |>. We will cover how
CSharp.Made.Functional includes a similar feature in later discussions.
Using the previous database example, let's now take a look at how we can use
the Result type to handle cases where the return type might be Ok and
sometimes it might be Error.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record NotFoundError(string Message);
public static class Program
{
// Let's use random to decide if finding the string succeeded or not.
// Imagine this is a database call or some other error prone operation.
public static Option<string> TryFindString(string input) =>
(new Random().Next(2) == 1) switch
{
true => Some(input),
false => None<string>()
};
public static Result<string, NotFoundError> StringFindingService(string input) =>
TryFindString(input)
.Map(Ok<string, NotFoundError>)
.Reduce(() =>
new NotFoundError($"'{input}' was not found.")
.Pipe(Error<string, NotFoundError>));
public static void Main()
{
for (var i = 0; i < 10; i++)
{
var result =
StringFindingService("Hello, Results!")
.Reduce(error => error.Message);
Console.WriteLine(result);
}
}
}
|
The example shows how we can use Ok and Error to return a
Result type. In C#, we need to be a little bit more explicit about the
types to make the compiler happy, so this is why the example is using
Ok<string, NotFoundError> where in F# that wouldn't be needed. Astute
observers would also notice that there are similar methods on Result as
there are on Option, which is quite on purpose. The aim of this library
is to be consistent and easy to use. It will feel much more natural to reach
for Match, Map, and Reduce when they're used for multiple types throughout
the library.
Ok and Error
To create a Result that is either Ok or Error, simply use the convenience
methods on the Prelude class. As shown below, the type signatures can get quite
long sometimes. In most cases I prefer to use var instead of the specific type,
but for clarity I have shown them here. In the example below, I have created
a simple NotFoundError type, but using what was discussed in the Union
section, this could also easily be a Union type with multiple error variants.
This is where the Result type really shines.
| Program.cs |
|---|
| using Functional.Results;
namespace ScratchPad;
public record NotFoundError(string Message);
public static class Program
{
public static void Main()
{
Result<string, NotFoundError> ok = Ok<string, NotFoundError>("It's okay");
Result<string, NotFoundError> error =
Error<string, NotFoundError>(new NotFoundError("An error message"));
}
}
|
Map and Reduce
Just like the Option type, Result has Map and Reduce methods. Map
works exactly the same way, in that if the Result is Ok, then it performs
the mapping function on the inner contents. Reduce, however has multiple
overloaded methods. It can be used in a way that discards the error and returns
an alternate value, or it can use the error to then create the alternate value.
The Map function can be called multiple times to perform different
transformations therefore pipelining the values from the previous transformation
to the next one. This can greatly improve the readability of what is happening to
a value as it goes through the pipeline. The example below is contrived in that
you could perform the mapping steps all in one operation, but for the sake of
demonstration, it's been broken out into multiple Map operations.
TryPayForMovies returns a Result<int, InsufficientFundsError> which for our
purposes means that sometimes the balance is Ok and sometimes, usually
according to some business or domain logic, it could be an Error. Through
multiple Map steps, we can convert the number to a string, add a $, then
add some more formatting for readability through the power of composition. To
demonstrate various cases, the example uses Enumerable.Range to print
different results 10 times.
| Program.cs |
|---|
| using System;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record InsufficientFundsError(string Message);
public static class Program
{
public static Result<int, InsufficientFundsError> TryPayForMovies() =>
// Let's assume that movies cost $25
new Random().Next(100) switch
{
var balance when balance > 25 =>
(balance - 25)
.Pipe(Ok<int, InsufficientFundsError>),
var balance =>
new InsufficientFundsError($"Movie Error: balance of ${balance} is too low to pay for movies.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static void Main() =>
Enumerable
.Range(0, 30)
.ToList()
.ForEach(_ =>
TryPayForMovies()
.Map(balance => balance.ToString())
.Map(strBalance => $"${strBalance}")
.Map(withDollarSign => $"Your balance after paying for the movies: {withDollarSign}")
.Reduce(err => err.Message)
.Tap(Console.WriteLine)
.Ignore());
}
|
Map Errors
In some cases, it will be necessary to convert one error type to another,
just like when performing a regular mapping function. In this case, reach
for the MapError method.
| Program.cs |
|---|
| using System;
using System.Collections.Generic;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record InsufficientFundsError(string Message);
public record ApplicationError(List<string> Errors);
public static class Program
{
public static Result<int, InsufficientFundsError> TryPayForMovies() =>
// Let's assume that movies cost $25
new Random().Next(100) switch
{
var balance when balance > 25 =>
(balance - 25)
.Pipe(Ok<int, InsufficientFundsError>),
var balance =>
new InsufficientFundsError($"Movie Error: balance of ${balance} is too low to pay for movies.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static void PrintErrorMessages(this Result<string, ApplicationError> result)
{
result
.Effect(
ok => { },
err => err.Errors.ForEach(Console.WriteLine)
);
}
public static ApplicationError ToApplicationError(this InsufficientFundsError error)
{
var errorMessage = error.Message;
var errorList = new List<string> { errorMessage };
return new ApplicationError(errorList);
}
public static void Main() =>
Enumerable
.Range(0, 30)
.ToList()
.ForEach(_ =>
TryPayForMovies()
.Map(balance => balance.ToString())
.Map(strBalance => $"${strBalance}")
.Map(withDollarSign => $"Your balance after paying for the movies: {withDollarSign}")
.MapError(err => err.ToApplicationError())
.Tap(err => err.PrintErrorMessages())
.Ignore());
}
|
Result Match
The previous example can be rewritten using Match as well. However, as previously
noted, Map can be called many times, where Match can only be called once.
Match should be thought of as a replacement for a single Map and Reduce
operation. This way, if there are many Map operations to perform, it is possible
to do all of them except the last one, then call Match.
| Program.cs |
|---|
| using System;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record InsufficientFundsError(string Message);
public static class Program
{
public static Result<int, InsufficientFundsError> TryPayForMovies() =>
// Let's assume that movies cost $25
new Random().Next(100) switch
{
var balance when balance > 25 =>
(balance - 25)
.Pipe(Ok<int, InsufficientFundsError>),
var balance =>
new InsufficientFundsError($"Movie Error: balance of ${balance} is too low to pay for movies.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static void Main() =>
Enumerable
.Range(0, 30)
.ToList()
.ForEach(_ =>
TryPayForMovies()
.Map(balance => balance.ToString())
.Map(strBalance => $"${strBalance}")
.Match(
withDollarSign => $"Your balance after paying for the movies: {withDollarSign}",
err => err.Message)
.Tap(Console.WriteLine)
.Ignore());
}
|
Result Bind
Just like the Bind method for Option, Result also has a Bind method for
when a mapping operation produces another Result type. There is a current
limitation on this method where the binding method must also produce the same
error type. This usually isn't a problem, but something to be aware of. Much of
this limitation can be overcome by using Discriminated Unions for an error type.
This way, a binding method could produce a different error perhaps, but if it's a
variant of a discriminated union, C# will still see it as the parent discriminated
union type. In the following example, let's say that we wanted to pay for a movie
and buy dinner. Buying dinner though, depends on whether or not there was money
left over after paying for the movies.
| Program.cs |
|---|
| using System;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record InsufficientFundsError(string Message);
public static class Program
{
public static Result<int, InsufficientFundsError> TryPayForMovies() =>
// Let's assume that movies cost $25
new Random().Next(100) switch
{
var balance when balance > 25 =>
(balance - 25)
.Pipe(Ok<int, InsufficientFundsError>),
var balance =>
new InsufficientFundsError($"Movie Error: balance of ${balance} is too low to pay for movies.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static Result<int, InsufficientFundsError> TryBuyDinner(int balance) =>
// assuming dinner costs $50
balance switch {
var bal when bal >= 50 =>
(balance - 50)
.Pipe(Ok<int, InsufficientFundsError>),
_ =>
new InsufficientFundsError($"Dinner Error: balance of ${balance} was too low to pay for dinner.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static void Main() =>
Enumerable
.Range(0, 30)
.ToList()
.ForEach(_ =>
TryPayForMovies()
.Bind(TryBuyDinner)
.Map(balance => balance.ToString())
.Map(strBalance => $"${strBalance}")
.Map(withDollarSign => $"Your balance after paying for the movies: {withDollarSign}")
.Reduce(err => err.Message)
.Tap(Console.WriteLine)
.Ignore());
}
|
Result Effect
Just like Option and Union, Result also has an Effect method that
allows us to perform some Action which returns void as a side-effect on
our Result type. Effect is similar to Match, but instead of returning
value, both arms return void. We can use Effect in place of the Map,
Reduce, and Tap methods used in the previous example.
| Program.cs |
|---|
| using System;
using System.Linq;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record InsufficientFundsError(string Message);
public static class Program
{
public static Result<int, InsufficientFundsError> TryPayForMovies() =>
// Let's assume that movies cost $25
new Random().Next(100) switch
{
var balance when balance > 25 =>
(balance - 25)
.Pipe(Ok<int, InsufficientFundsError>),
var balance =>
new InsufficientFundsError($"Movie Error: balance of ${balance} is too low to pay for movies.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static Result<int, InsufficientFundsError> TryBuyDinner(int balance) =>
// assuming dinner costs $50
balance switch
{
var bal when bal >= 50 =>
(balance - 50)
.Pipe(Ok<int, InsufficientFundsError>),
_ =>
new InsufficientFundsError($"Dinner Error: balance of ${balance} was too low to pay for dinner.")
.Pipe(Error<int, InsufficientFundsError>)
};
public static void Main() =>
Enumerable
.Range(0, 30)
.ToList()
.ForEach(_ =>
TryPayForMovies()
.Bind(TryBuyDinner)
.Map(balance => balance.ToString())
.Map(strBalance => $"${strBalance}")
.Effect(
withDollarSign =>
Console.WriteLine($"Your balance after paying for dinner and the movies: {withDollarSign}"),
err => Console.WriteLine(err.Message)));
}
|
Result EffectOk and EffectError
Perform an Effect when a Result is Ok or when it is an Error.
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using static Functional.Prelude;
using System;
public static class Program
{
public static void Main()
{
// This will print "value" to the console.
Ok("value")
.EffectOk(value => Console.WriteLine(value));
// This will do nothing since the input value was an Error.
Error<string>(new Exception("Something bad happened"))
.EffectOk(value => Console.WriteLine(value));
// This will do nothing since the input value is Ok
Ok("value")
.EffectError(exception => Console.WriteLine(exception.Message));
// This will print "Something bad happened" since it was an error.
Error<string>(new Exception("Something bad happened"))
.EffectError(exception => Console.WriteLine(exception.Message));
}
}
|
Result Tap, TapOk, and TapError
If we want to Tap into the Result and perform some effect without consuming the value, we can use Tap, TapOk, and TapError. With Tap, some kind of action must be provided for both the Ok and Error cases.
For TapOk and TapError, one or more actions can be provided to occur when the Result meets that criteria. This will allow us to only perform actions when the value is Ok or Error for instance.
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using static Functional.Prelude;
using System;
public static class Program
{
public static void Main()
{
// Actions can be performed when ok and error.
Ok("value")
.Tap(ok => Console.WriteLine(ok), exception => Console.WriteLine(exception.Message))
// The Result is not consumed so we can still use it afterwards.
.Map(ok => ok + "!");
// We can do multiple things when the value is Ok with a TapOk.
string? temp = null;
Ok("value")
.TapOk(
value => Console.WriteLine(value),
value => temp = value);
// Nothing happens here since the value is an error.
// Error<T> creates a Result<T, Exception> by accepting a string or an Exception.
// The value "Error!" is used as the exception message.
Error<string>("Error!")
.TapOk(value => Console.WriteLine(value));
}
}
|
Result Unwrap and UnwrapError
If we need to get the value out of a Result for some reason and it's impractical to use Match, Map, Tap, or Effect, we can Unwrap or UnwrapError in order to get its inner contents.
It's vital to check to see if the Result is Ok before using Unwrap and see if it's Error before using UnwrapError, otherwise it will throw an exception!
| Program.cs |
|---|
| namespace ScratchPad;
using static Functional.Prelude;
using Functional;
public static class Program
{
public static void Main()
{
// This will unwrap fine because the value is Ok.
string value = Ok("value").Unwrap();
// This will throw an exception because the value is an error.
string never = Error<string>("Error!").Unwrap();
// To do this safely, we need to always check the Option first!
var result = Error<string>("Error!");
if (result.IsOk)
{
value = result.Unwrap();
}
if (result.IsError)
{
value = result.UnwrapError().Message;
}
}
}
|
Async Results
Asynchronous support is also provided in this library for Result.
Included async methods:
MapAsync
ReduceAsync
MatchAsync
BindAsync
EffectAsync
EffectOkAsync
EffectErrorAsync
TapAsync
TapOkAsync
TapErrorAsync
UnwrapAsync
UnwrapErrorAsync
| Program.cs |
|---|
| using System;
using System.Threading.Tasks;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record CustomError(string Message);
public static class Program
{
public static async Task Main() =>
await Ok<string, CustomError>("Ok")
.Async()
.MapAsync(ok => ok + "!")
.EffectAsync(
ok => Console.WriteLine(ok),
err => Console.WriteLine(err.Message));
}
|
Common Extensions
Throughout these examples, there have been a few extension methods that have
made functional programming easier to work with in C#. Let's talk about a few
of those in detail now.
Pipe
Pipe is a general-purpose mapping function that works with any type. Due to
limitations and naming conflicts in C#, using Map again for this purpose
was not possible. Because of this, I chose to use the name Pipe to match the
F# |> pipe operator. Pipe allows us to take the results of a previous function,
transformation, or other expression and then perform additional transformations
on it. Here is a simple example demonstrating its use. There is also a
PipeAsync method for async processing as well.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record CustomError(string Message);
public static class Program
{
public static int OneMillion => 1_000_000;
public static void Main() =>
OneMillion
.Pipe(money => string.Format("{0:C}", money))
.Tap(Console.WriteLine)
.Ignore();
}
|
Tap
In the previous example, we used the Tap method to perform a side-effect
the output of the Pipe method. The way that Tap works, is that it will
perform operations on the output of the previous value, and then return that
value as its output. For immutable types, the output will be unchanged. However,
be warned that if the action performed in the Tap method mutates the input,
then the output will also have mutated values. Below is an example with a mutable
property to demonstrate this behavior. There is also a TapAsync method for
async processing as well.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public class IntClass
{
public int Value { get; set; }
}
public static class Program
{
public static int OneMillion => 1_000_000;
public static void Main()
{
var classValue =
OneMillion
.Pipe(number => new IntClass { Value = number })
.Tap(intClass => intClass.Value += 1)
.Pipe(intClass => intClass.Value);
// Prints "1000001" because the class value was mutated.
Console.WriteLine(classValue);
}
}
|
Tap allows multiple actions to provided so that many things can be done to
the input at once. Here is an example demonstrating adding 1 and then printing
the results before saving the value in a variable.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public class IntClass
{
public int Value { get; set; }
}
public static class Program
{
public static int OneMillion => 1_000_000;
public static void Main()
{
var classValue =
OneMillion
.Pipe(number => new IntClass { Value = number })
.Tap(
intClass => intClass.Value += 1,
intClass => Console.WriteLine($"Printed In Tap: {intClass.Value}"))
.Pipe(intClass => intClass.Value);
// Prints "1000001" because the class value was mutated.
Console.WriteLine($"Printed at the end: {classValue}");
}
}
|
Effect
Effect is like a Pipe that consumes the input, performs some series
of actions, and returns Unit.
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using System;
using static Functional.Prelude;
public static class Program
{
public static void Main()
{
"Some Random Value"
.Effect(input => Console.WriteLine(input));
Effect(() => Console.WriteLine("another way."))
.Pipe(unit => "It's a unit type!");
EffectAsync(() => Console.WriteLine("This one returns a Task<Unit>!"))
.PipeAsync(unit => "It's another unit!");
}
}
|
Cons
Cons generates an ImmutableList of any type that you put in it. In .NET
8 and C# 12, Collection Expressions and Collection Literals help reduce the
need for this, but it can still be useful in older versions. See example
below for usage.
| Program.cs |
|---|
| using System;
using static Functional.Common.CommonExtensions;
namespace ScratchPad;
public static class Program
{
public static void Main()
{
Cons("some", "things", "to", "print")
.ForEach(Console.WriteLine);
}
}
|
Ignore
Ignore and IgnoreAsync are used to ignore the output of a function. In languages like F#, any unused values must be explicitly ignored. In C#, this isn't required. To indicate that a calculated result is ignored, you can add this to the end of the function. Ignore produces void and IgnoreAsync produces a Task.
| Program.cs |
|---|
| using System.Threading.Tasks;
using static Functional.Common.CommonExtensions;
namespace ScratchPad;
public static class Program
{
public static async Task Main()
{
"Some Contents"
.Pipe(str => str + "!")
.Ignore();
await "Some Async Contents"
.Async()
.PipeAsync(str => str + "!")
.IgnoreAsync();
}
}
|
Exception Handling
When interacting with code that can throw Exceptions, we normally reach for the
traditional Try/Catch/Finally block. CSharp.Made.Functional includes a few
methods to deal with exceptions in a more fluent style.
Try
Use Try to perform an operation which could throw an Exception. There are two
variants to this method. First, it can be used as a plain static method
which expects some function to be performed that returns some kind of value.
There is also an extension method that allows a previous value to be used as
input to Try which is shown in a later example. The return type of these
methods is Result<TResult, Exception> where TResult is the type that the
operation returns. Since there are already a lot of useful methods available
on Result, it makes working with basic Try/Catch work simpler.
CSharp.Made.Functional does not provide any mechanisms for a Finally
block and it's recommended to use the standard Try/Catch/Finally approach in
those cases. Since overly broad Exception catching is not a best practice,
it is recommended to use a Switch Expression when matching on the Result
to handle specific exceptions that are expected for this operation.
In the example below, since we think it could throw, we want to use
the Result type to help us determine if it was Ok or an Error. We can
make decisions in the Catch handler as to whether or not we want to return
an Error or we can also throw if it truly is a catastrophic exception.
Like all of the other methods in this library, there are also async methods
which work the same way.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record CustomError(string Message);
public static class Program
{
public static int ItMightThrow() =>
new Random().Next(100) switch
{
var value when value > 50 => value,
var value => throw new Exception($"Value was {value}")
};
public static void Main() =>
Try(ItMightThrow)
.Match(
ok => ok.Pipe(Result.Ok<int, CustomError>),
exception =>
{
// Example logging.
Console.WriteLine(exception.Message);
exception
.InnerExceptionMessage()
.Effect(
err => Console.WriteLine(err),
() => { /* It was none, don't print anything. */ });
return (exception switch
{
ArgumentNullException => "It was null",
OperationCanceledException => "It was cancelled",
_ => "We don't know why it crashed..."
})
.Pipe(msg => new CustomError(msg))
.Pipe(Result.Error<int, CustomError>);
})
.Match(ok => ok.ToString(), err => err.Message)
.Tap(Console.WriteLine)
.Ignore();
}
|
Try can also be used as an extension method in order to add a Try/Catch
handler to the end of a function that isn't expected to throw.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record CustomError(string Message);
public static class Program
{
public static int ItNeverThrows() =>
new Random().Next(100);
public static int ItMightThrow(int input) =>
input switch
{
var value when value > 50 => value,
var value => throw new Exception($"Value was {value}")
};
public static void Main() =>
ItNeverThrows()
.Try(ItMightThrow)
.Match(
ok => ok.Pipe(Result.Ok<int, CustomError>),
exception =>
{
// Example logging.
Console.WriteLine(exception.Message);
exception
.InnerExceptionMessage()
.Effect(
err => Console.WriteLine(err),
() => { /* It was none, don't print anything. */ });
return (exception switch
{
ArgumentNullException => "It was null",
OperationCanceledException => "It was cancelled",
_ => "We don't know why it crashed..."
})
.Pipe(msg => new CustomError(msg))
.Pipe(Result.Error<int, CustomError>);
})
.Match(
ok => ok.ToString(),
err => err.Message)
.Tap(Console.WriteLine)
.Ignore();
}
|
Inner Exception Messages
Exceptions may or may not have an inner exception message. There is a convenience method called InnerExceptionMessage() which returns Option<string> to safely handle getting an inner exception method.
| Program.cs |
|---|
| using System;
using Functional;
using static Functional.Prelude;
namespace ScratchPad;
public record CustomError(string Message);
public static class Program
{
public static void Main()
{
new Exception("outer message", new Exception("Inner message"))
.InnerExceptionMessage()
.Effect(
// This will print because there is an inner exception.
Console.WriteLine,
() => { /* There was no inner exception */ });
// Nothing will print here because there was no inner exception.
new Exception("outer message")
.InnerExceptionMessage()
.Effect(
Console.WriteLine,
() => { /* There was no inner exception */ });
}
}
|
Unit
When performing an action, instead of returning void, we can return the type called Unit which represents no return value. This can be used to continue piping more functions after performing an action that would return void. Calling the second Effect would not have been possible without the Unit type.
| Program.cs |
|---|
| namespace ScratchPad;
using Functional;
using static Functional.Prelude;
using System;
public static class Program
{
public static void Main()
{
// This should print "value" and then "()" on the following line.
Some("value")
.Effect(value => Console.WriteLine(value), () => Console.WriteLine("No value"))
.Effect(unit => Console.WriteLine(unit.ToString()));
}
}
|