Skip to content

Write Honest functions in C# or convert Pure functions to Honest functions using Optional and Result monads. Combine them with LINQ and create extremely powerful functional compositions.

License

Notifications You must be signed in to change notification settings

singh-rajiv/honesty-dotnet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

honesty-dotnet

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.

What are pure functions?

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.

What are honest functions?

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

Installation

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

Optional<T> type

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

Functional composition using Optional<T>

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

Result<T> type

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

Functional composition using Result<T>

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

Unit type

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

Other functional methods

Both the types provide standard methods found in functional programming - Match, Map, Bind, Where etc, DefaultIfNone and DefaultIfException. All methods have asynchronous overloads.

About

Write Honest functions in C# or convert Pure functions to Honest functions using Optional and Result monads. Combine them with LINQ and create extremely powerful functional compositions.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages