Luger.Functional.Maybe
2.0.1
dotnet add package Luger.Functional.Maybe --version 2.0.1
NuGet\Install-Package Luger.Functional.Maybe -Version 2.0.1
<PackageReference Include="Luger.Functional.Maybe" Version="2.0.1" />
<PackageVersion Include="Luger.Functional.Maybe" Version="2.0.1" />
<PackageReference Include="Luger.Functional.Maybe" />
paket add Luger.Functional.Maybe --version 2.0.1
#r "nuget: Luger.Functional.Maybe, 2.0.1"
#addin nuget:?package=Luger.Functional.Maybe&version=2.0.1
#tool nuget:?package=Luger.Functional.Maybe&version=2.0.1
Luger.Functional.Maybe
Luger.Functional.Maybe<T>
is a composable version of System.Nullable<T>
for
value types and non-nullable reference types.
The value of Maybe<T>
can be in one of two main states; some or none.
Some is analogous to a Nullable<T>
or a nullable reference with a value.
None is analogous to a Nullable<T>
or a nullable reference without a value,
i.e. the infamous null
.
Why?
The primary reason for using values of Maybe<T>
instead of Nullable<T>
or
nullable reference types is safety. C# is a relatively type safe language but
null
is one piece of the language design where it breaks.
null
has no type. A function returning null
instead of a value of its return
type is essentially dishonest.
The problems with null
are explained in, sometimes humorous and sometimes
painful, detail all around the interwebs. I'll not bother you with it here.
Instead, here are a couple of examples of usage together with the traditional C# approach.
Handling "null" return value
In a lot of C# code null
is returned from a function to signal when the happy
path did not work out. In such cases the author of calling code must remember to
implement null-check or their code may throw an exception.
Thing FindThing(ThingId id) {...}
void consuming_code()
{
ThingId id = ...;
var thing = FindThing(id);
if (thing is null)
{
// Handle thing not found
}
else
{
// Handle found thing
}
}
The possibility of FindThing
returning null
is not expressed by its
signature. The author of consuming code must read its implementation or
documentation, neither of which may be available, to find out.
In contrast, using Maybe<T>
informs the author of consuming code that there is
a possibility of not finding a thing and to handle the thing found the return
value must be matched against.
Maybe<Thing> FindThing(ThingId id) {...}
void consuming_code()
{
ThingId id = ...;
if (FindThing(id) is [var thing])
{
// Handle found thing
}
else
{
// Handle thing not found
}
}
Here, thing
is only in scope within the happy path where a thing was found and
the calling code does not risk dereferencing a null
value.
Here, even though thing
is in scope in the else
block, the compiler should
give you an error
(CS0165)
about it being unassigned if you try to access it in the else
block.
Composing computations of "null"
It is common to have computations composed from a series of steps where each step depend on results from previous steps and may result in an unhappy outcome.
Result Computation(Input input)
{
var result1 = Step1(input);
if (result1 is null)
{
return null;
}
var result2 = Step2(result1);
if (result2 is null)
{
return null;
}
return Step3(result2);
}
I've seen attempts to clean this up by letting the Step#
functions handle
null
inputs and just pass null
on as their result.
Result Computation(Input input) => Step3(Step2(Step1(input)));
The main problem with this, besides being ugly, is that now all the Step#
functions have dishonest signatures not only with regard to return value but
also their parameter.
Instead, if the steps return Maybe<T>
we can use its composability to
implement the computation as a quite simple and elegant expression.
Maybe<Result> Computation(Input input)
=> from r1 in Step1(input)
from r2 in Step2(r1)
from r3 in Step3(r2)
select r3;
Which is precompiled to something like the following.
Maybe<Result> Computation(Input input)
=> Step1(input)
.SelectMany(r1 => Step2(r1), (r1, r2) => new { r1, r2 })
.SelectMany(r1r2 => Step3(r1r2.r2), (r1r2, r3) => r3);
Or, if one dislikes LINQ query syntax, one can bind this, admittedly simple, process explicitly.
Maybe<Result> Computation(Input input) => Step1(Input).Bind(Step2).Bind(Step3);
This style of composition of sequentially dependent computation is called
monadic and is possible to implement for monadic types like Maybe<T>
as they
implement Bind
(and SelectMany
for the LINQ query syntax support).
When inputs to a computation are sequentially independent their composition can
also be performed in an applicative style. This is possible for types which
are applicative functors (which Maybe<T>
is) as they implement Apply
.
A trivial example is the following computation of the sum of two Maybe<int>
inputs.
Maybe<int> Sum(Maybe<int> maybeX, Maybe<int> maybeY)
{
var maybeSum = Some((int x, int y) => x + y);
return maybeSum.Apply(maybeX).Apply(maybeY);
}
Pattern matching
You can pattern match against values of Maybe<T>
by using C# 11
List Patterns
Console.WriteLine(maybeT is [var t] ? $"Got some {t}!" : "Got none.");
Console.WriteLine(maybeT is [] ? "Got none." : "Got some!");
Equality
Maybe<T>
implements IEquatable<Maybe<T>>
and overrides
object.Equals(object?)
.
The comparison works in the same way as equality comparison in Nullable<T>
.
Delegation of formatting to underlying type
Maybe<T>
implements IFormattable
which Maybe<T>.ToString()
and
Maybe<T>.ToString(string?)
delegates to.
IFormattable.ToString(string?, IFormatProvider?)
delegates to the same method
on the value if T
is IFormattable
; otherwise object.ToString()
is used to
produce the value representation.
Some is represented as "[<value>]"
.
None is represented as "[]"
.
Playing nice with System.Linq.Enumerable
Maybe<T>
implements IEnumerable<T>
. The enumerator will yield zero or one
element for none or some respectively.
This enables Maybe<T>
to be functionally bound (flattened) together with any
IEnumerable<T>
.
var flattened = from x in xs from t in funcMaybe(x) select t; // some results from funcMaybe(x) are filtered
Operators
Maybe<T>
implements truth (true
, false
) and logical conjunction (&
) and
disjunction (|
) operators.
The combination also provides conditional logical operators (&&
, ||
). This
enables chaining of Maybe<T>
values in logical expressions.
Using the conditional operators enables lazy evaluation as expected.
The disjunction (|
) operator is also implemented between Maybe<T>
and T
.
This is useful to provide a fallback value, much like
Nullable<T>.GetValueOrDefault(T)
Since C# cannot handle conditional logical operators of operands of different
types, another overload of the disjunction operator is introduced in v1.1.0 to
help with lazy evaluation of fallback value. It provides functionality much like
maybeX || getZ()
would, where getZ
is a function providing the fallback
value.
Some illustrations;
maybeX & maybeY
evaluates to maybeY
if maybeX
is some; otherwise none.
maybeX | maybeY
evaluates to maybeX
if it is some; otherwise maybeY
.
maybeX && getMaybeY()
evaluates to the result of getMaybeY()
if maybeX
is
some; otherwise getMaybeY
is not invoked and the result is none.
maybeX || getMaybeY()
evaluates to maybeX
if it is some; otherwise
the result of getMaybeY()
.
maybeX | y
evaluates to the value of maybeX
if it is some; otherwise y
.
maybeX | getY
where getY
is a function returning a value of T
evaluates to
the value of maybeX
if it is some; otherwise the result of getY()
.
Maybe<T>
implements implicit cast operator from T
.
Thus, returning some value from a Maybe<T>
-returning function is no effort.
Returning none from a Maybe<T>
-returning function is equally simple as it is
the default state; return default;
.
Factories
The static class Maybe
implement these factory methods.
Maybe<T> None<T>() where T : notnull
Produce the none value. Equivalent to default(Maybe<T>)
.
Maybe<T> Some<T>(T value) where T : notnull
Produce a some value for the given value
. Equivalent to implicit cast
from T
to Maybe<T>
.
Maybe<T> FromNullable<T>(T? value) where T : struct
Convert a Nullable<T>
value to a Maybe<T>
value.
Maybe<T> FromReference<T>(T? value) where T : class
Convert a nullable reference to a Maybe<T>
value.
Extensions
The static class Maybe
also implement the following extension methods.
Apply
Maybe<TResult> Apply<TArg, TResult>(
this Maybe<Func<TArg, TResult>> maybeFunc,
Maybe<TArg> maybeArg)
Apply a lifted function to a lifted parameter.
The unary Apply
corresponds to the infix operator <*>
of Applicative in
Haskell. If you need to apply higher arity functions you'll have to curry them
yourself.
Bind
Maybe<TResult> Bind<TSource, TResult>(
this Maybe<TSource> source,
Func<TSource, Maybe<TResult>> func)
Monadic composition of the computation of Maybe<TSource>
over the application
of a Maybe<TResult>
-returning function.
Bind
corresponds to the infix operator >>=
of Monad in Haskell.
Maybe<TResult> SelectMany<TSource, TNext, TResult>(
this Maybe<TSource> source,
Func<TSource, Maybe<TNext>> selector,
Func<TSource, TNext, TResult> resultSelector)
Project the value of Maybe<TSource>
to a Maybe<TNext>
and invoke a result
selector function on the pair to produce the result. Provided for support of
LINQ query syntax. The expression
from s in source
from n in selector(s)
select resultSelector(s, n)
is precompiled into
source.SelectMany(selector, resultSelector)
The difference between Bind
and SelectMany
is that the latter takes a binary
projection function as a parameter and as such can chain calls instead of
encapsulating calls in nested closures.
Filter
Maybe<TSource> Filter<TSource>(
this Maybe<TSource> source,
Func<TSource, bool> predicate)
Filter a lifted value based on a predicate function.
Maybe<TSource> Where<TSource>(
this Maybe<TSource> source,
Func<TSource, bool> predicate)
Do exactly the same as Filter
but provided for support of LINQ query syntax.
The expression
from s in source
where predicate(s)
select s
is precompiled into
source.Where(predicate)
Map
Maybe<TResult> Map<TSource, TResult>(
this Maybe<TSource> source,
Func<TSource, TResult> func)
Map a lifted value of TSource
by given function to a lifted value of
TResult
.
Map
corresponds to the infix operator <$>
of Functor in Haskell.
Maybe<TResult> Select<TSource, TResult>(
this Maybe<TSource> source,
Func<TSource, TResult> selector)
Project the value of Maybe<TSource>
into a new form. This is exactly the same
functionality as Map
above but is provided for support of LINQ query syntax.
The expression
from s in source
select selector(s)
is precompiled into
source.Select(selector)
Try
bool Try<TSource>(this Maybe<TSource> source, out TSource value)
Provides Try
-style method syntax to extract the value of Maybe<TSource>
for
consuming code which is not able to use C# 11 list pattern matching.
Instead of the expression source is [var s]
such code can use
source.Try(out var s)
.
Nullable interop
T? ToNullable<T>(Maybe<T> value) where T : struct
Convert the Maybe<T>
value to a Nullable<T>
value.
T? ToReference<T>(Maybe<T> value) where T : class
Convert the Maybe<T>
value to a nullable reference.
IEnumerable<T>
extensions
Maybe<T> MaybeSingle<T>(this IEnumerable<T> source) where T : notnull
Maybe<T> MaybeSingle<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : notnull
Maybe<T>
-returning versions of System.Linq.Enumerable.SingleOrDefault
overloads.
Instead of returning default<T>
on empty input sequence they return None<T>
.
They throw an InvalidOperationException
on multiple elements just like their
BCL counterparts.
Maybe<T> MaybeFirst<T>(this IEnumerable<T> source) where T : notnull
Maybe<T> MaybeFirst<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : notnull
Maybe<T>
-returning versions of System.Linq.Enumerable.FirstOrDefault
overloads.
Instead of returning default<T>
on empty input sequence they return None<T>
.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. |
-
net8.0
- JetBrains.Annotations (>= 2024.3.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Luger.Functional.Maybe:
Package | Downloads |
---|---|
Luger.Async.ExponentialBackoff
Utility for configuring and awaiting exponential backoff over asynchronous functions. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Remove obsolete Apply overloads.
Change exception type in index property.