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()));
}
}
|