honesty-dotnet
is a collection of lightweight monads - Optional<T>
and Result<T>
which are immutable (readonly) structs, few simple extension methods of Task<T>
, and Unit
type which indicates the absence of a specific value generated by an expression (think void in OOP). These constructs can be used to convert pure functions to honest functions, write LINQ queries on even non-sequence data types and chain powerful functional compositions.
In computer programming pure functions are those functions that always return the same result for same input parameter values, the result does not depend on anything other than the input parameter values i.e. it does not depend on any other external state, class or global or static variable, and they do not have side-effects i.e. they do not cause any change to external state, class or global or static variable.
Honest functions are a step over and above pure functions. While they are also pure functions, they additionally let the consuming code handle scenarios like absense of a result or exception in computation of a result more gracefully than just pure functions. The behavior of pure functions is not really pure in these two scenarios i.e. it is dishonest.
Honest functions solve this problem by amplifying the normal return type T
of a pure function to special monadic types like Optional<T>
or Result<T>
which represent potential lack of a result or potential exception in computing the result respectively.
//1. Given: a PURE Function
//consider the below given pure function which returns the integer division numerator/denominator
//it promises to return an integer given 2 integers
int Divide(int numerator, int denominator) => numerator/denominator;
//2. Problem: the behavior is pure most of the times but not when a result is absent or there is an exception
var quotient1 = Divide(100, 5); //20, pure behavior
var quotient2 = Divide(100, 0); //throws DivideByZero exception, not pure behavior
//3. Solution: convert Pure Function to Honest Function
//consider using Result.Try to define a new pure AND honest Func which returns a Result<int>
//type of TryDivide is Func<int, int, Result<int>>
var TryDivide = (int n, int d) => Result.Try(() => Divide(n, d)); //is always honest
//type of result1 & result2 is Result<int>
var result1 = TryDivide(100, 5); //result1 'contains' 20, pure behavior
var result2 = TryDivide(100, 0); //result2 'contains' exception, also pure behavior
//TryDivide will always return promised Result and not throw exception
//4. Functional Composition
//Lets say we want to perform two divisions and compute the sum of quotients
//then we can create a functional composition using a LINQ query as below
//sum will contain a value only if both the divisions were successful
//otherwise it will contain exception from the faulting division operation
//type of sum is Result<int>
var sum = from q1 in TryDivide(n1, d1) //use the new Func
from q2 in TryDivide(n2, d2) //select clause executes only when the two divisions are successful
select q1 + q2; //type of q1 and q2 in the LINQ query is int not Result<int>
//sum could also be written using Method syntax
//but the LINQ query syntax is more concise and easier to read & comprehend
sum = TryDivide(n1, d1).
Bind(q1 => TryDivide(n2, d2). //Bind ~= SelectMany
Map(q2 => q1 + q2)); //Map ~= Select
//5. Pattern Matching
var (logLevel, msg) = sum.Match(
val => (LogLevel.Information, $"Success, val: {val}"),
ex => (LogLevel.Error, $"Error: {ex}"));
logger.Log(logLevel, msg);
It can be installed via Nuget Package Manager in Visual Studio or .NET CLI in Visual Studio Code.
PM > Install-Package HonestyDotNet
.NET CLI > dotnet add package HonestyDotNet
It is a monad that represents an amplified type T which may or may not contain a value.
There are multiple ways to create an Optional<T>
.
Using new
var o1 = new Optional<int>(5);
var o2 = new Optional<string>("Test");
var o3 = new Optional<object>(null); //does not contain a value
Using static methods
var o1 = Optional.Some(5);
var o2 = Optional.None(5); //does not contain a value
Using Try methods which take a pure function as an input. Any exception is caught but ignored.
Note: In the examples below many funcs take in a nullable string string?
and then use the null-forgiving operator !
to get rid of compiler warning about dereferencing a maybe null object. This is to just show that any exceptions thrown are caught
by the various Try methods. However, in the real world one would be expected to check nullable types for null before dereferencing or use non-nullable types.
//sync version
var o1 = Optional.Try(() => 5);
var o2 = Optional.Try((string? s) => s!.Length, null); //does not contain a value
//async version
static Task<int> Process(string? s) => Task.Run(() => s!.Length); //force NullReferenceException when s is null
var o1 = await Optional.Try(Process, "Test");
var o2 = await Optional.Try<string?, int>(Process, null); //does not contain a value
Using extension method
var s1 = "Hello";
string? s2 = null;
var o1 = s1.ToOptional();
var o2 = s2.ToOptional(); //does not contain a value
Based on the result of a boolean expression or variable
var flag = true;
var o1 = flag.IfTrue(() => 100);
flag = false;
var o2 = flag.IfTrue(() => 10); //does not contain a value
The real power of monads is in their ability to allow creating arbitrary functional compositions while keeping the code simple and concise, which makes the intent of the programmer conspicuous.
var sHello = "Hello";
var sWorld = "World";
string? sNull = null;
//an arbitrary task that yields an int when run to succesful completion
//by calculating some arbitrary tranformation on a given string
async Task<int> AsyncCodeOf(string? i) => await Task.Run(() => (int)Math.Sqrt(i!.GetHashCode()));
//honesty-dotnet enables using LINQ query syntax on Task type and this does not require any extra effort on developer's part
//the return type of Optional.Try call below is Task<Optional<int>>
var r1 =
await
from maybeValue1 in Optional.Try(AsyncCodeOf, sHello)
from maybeValue2 in Optional.Try(AsyncCodeOf, sWorld)
from maybeValue3 in Optional.Try(AsyncCodeOf, sHello + sWorld)
select //this select clause executes only after all tasks above have completed
(
from value1 in maybeValue1
from value2 in maybeValue2
from value3 in maybeValue3
select value1 + value2 + value3 //this select clause executes only if all 3 values exist
);
Assert.True(r1.IsSome); //result r1 contains a value
var r2 =
await
from maybeValue1 in Optional.Try(AsyncCodeOf, sHello)
from maybeValue2 in Optional.Try(AsyncCodeOf, sNull) //this task will fail with an exception
from maybeValue3 in Optional.Try(AsyncCodeOf, sHello + sWorld)
select
(
from value1 in maybeValue1
from value2 in maybeValue2
from value3 in maybeValue3
select value1 + value2 + value3 //this select clause does not execute
);
Assert.False(r2.IsSome); //result r2 does not contain a value
It is a monad that represents an amplified type T which either contains a value or an exception that was thrown trying to compute the value.
There are multiple ways to create a Result<T>
.
Using new
var ex = new Exception("Something happened");
var r1 = new Result<int>(5); //contains value
var r2 = new Result<int>(ex); //contains exception
Using static methods
var ex = new Exception("Something happened");
var r1 = Result.Value(10); //contains value
var r2 = Result.Exception<int>(ex); //contains exception
Using Try methods which take a pure function as an input. Any exception is caught and captured on the return type.
//sync version
static int LengthOf(string? s) => s!.Length; //force NullReferenceException when s in null
var r1 = Result.Try(LengthOf, "Hello"); //contains value
var r2 = Result.Try<string?, int>(LengthOf, null); //contains exception
//async version
static Task<int> LengthOf(string? s) => Task.Run(() => s!.Length);
var r3 = await Result.Try(LengthOf, "Hello"); //contains value
var r4 = await Result.Try<string?, int>(LengthOf, null); //contains exception
Using extension method
var ex = new Exception("Something happened");
var i = 5;
var r1 = i.ToResult(); //contains value
var r2 = ex.ToResult<int>(); //contains exception
The real power of monads is in their ability to allow creating arbitrary functional compositions while keeping the code simple and concise, which makes the intent of the programmer conspicuous.
var sHello = "Hello";
var sWorld = "World";
string? sNull = null;
//an arbitrary task that yields an int when run to succesful completion
//by calculating some arbitrary tranformation on a given string
async Task<int> AsyncCodeOf(string? s) => await Task.Run(() => (int)Math.Sqrt(s!.GetHashCode()));
//honesty-dotnet enables using LINQ query syntax on Task type and this does not require any extra effort on developer's part
//the return type of Result.Try call below is Task<Result<int>>
var r1 =
await
from exOrVal1 in Result.Try(AsyncCodeOf, sHello)
from exOrVal2 in Result.Try(AsyncCodeOf, sWorld)
from exOrVal3 in Result.Try(AsyncCodeOf, sHello + sWorld)
select //this select clause executes only after all tasks above have completed
(
from val1 in exOrVal1
from val2 in exOrVal2
from val3 in exOrVal3
select val1 + val2 + val3 //this select clause executes only if all 3 values exist
);
Assert.True(r1.IsValue); //result r1 contains value
var r2 =
await
from exOrVal1 in Result.Try(AsyncCodeOf, sHello)
from exOrVal2 in Result.Try(AsyncCodeOf, sNull) //this task will fail with an exception which is captured
from exOrVal3 in Result.Try(AsyncCodeOf, sHello + sWorld)
select
(
from val1 in exOrVal1
from val2 in exOrVal2
from val3 in exOrVal3
select val1 + val2 + val3 //this select clause does not execute
);
Assert.False(r2.IsValue);
Assert.NotNull(r2.Exception); //result r2 contains exception thrown above in task2
The Unit
type indicates absence of a return value from a function or expression evaluation and resembles the void
type in OOP languages like C++, C# and Java. The ActionExtensions
class defines a bunch of ToFunc
extension methods on Action
delegates which can be use to convert an Action
into a Unit
returning Func
delegate.
var ex = new Exception("Something happened");
var r1 = new Result<string>("HelloWorld");
var r2 = new Result<string>(ex);
bool whenValueCalled;
bool whenExCalled;
string? argStr;
Exception argEx = new();
//a bunch of void returning methods
void Reset() =>
(whenValueCalled, whenExCalled, argStr) = (false, false, null);
void whenValue(string s) =>
(argStr, whenValueCalled) = (s, true);
void whenEx(Exception ex) =>
(argEx, whenExCalled) = (ex, true);
//coupld of Action delegates
var valAction = whenValue;
var exAction = whenEx;
Reset();
//covert Action delegates to Func delegates using ToFunc extension methods
var u = r1.Match(valAction.ToFunc(), exAction.ToFunc()); //pattern match Result r1
Assert.Equal(Unit.Instance, u);
Reset();
u = r2.Match(valAction.ToFunc(), exAction.ToFunc()); //pattern match Result r2
Assert.Equal(Unit.Instance, u);
Both the types provide standard methods found in functional programming - Match
, Map
, Bind
, Where
etc, DefaultIfNone
and DefaultIfException
. All methods have asynchronous overloads.