install-package DCP.CoMonad
Combinatorial Monads for Result, Task, ValueTask, Linq and IAsyncEnumerable.
The most succinct powerful functional library.
Inspired by Scott Wlaschin and railway oriented program design principles.
Also includes Discriminated Union (OR Type) to assist with Domain design.
Uses latest features of C# to adopt new paradigms based on functional monadic design. Retains minimalist design. Easy learning and migration curve.
Differs from other functional libraries in succinctness without loss of power.
DotNet Core .csproj file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<LangVersion>8.0</LangVersion>
</PropertyGroup>
...
</Project>
DotNet CLR .csproj file
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
...
<LangVersion>8.0</LangVersion>
</PropertyGroup>
...
</Project>
- Functional
- Introduction
- No Exception
- No Option
- No Complexity
- Failure!
- Worlds Collide
- Unionize
- Monads All the Way
- Reference Videos by others
What is functional programming anyway?
-
Style
A style that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
Because it is just a style there is no requirement to use a 'functional language', although these may have defaults to immutability and even compiler restrictions that simplify or enforce the style.
The terms 'Pure', 'deterministic', 'referential transparency' are fancy descriptors for this style.
-
First Class Functions and Higher-order functions
An ability to pass and receive functions as parameters.
.Net and indeed most languages possess this ability. Func<T..n> and delegates represent this ability in .Net
The important thing to note is that .Net (C# in particular) is fully capable of functional programming style and technique.
Functional programming style primary benefit is enhancing code readability and ability to reason about.
The composition throughout our programs is really important. We need to be confident that each piece of code and the composition of code is correct.
Having one small piece of code that is easy to read and reason about is very easy to achieve in practically any dynamic.
Dealing with large code bases maintained by multiple developers is intrinsically more difficult.
Functional style shines 😊 in this regard.
Using the Result<T> struct type. This type represents a discriminated union of either success or error and allows us to adopt railway oriented programming. Chain function calls and reduce code paths to a single railway track.
Result<T> implements Map, Bind, Combine and their async counter-parts
With these few methods we can transform code to more functional happy paths.
eg Checked Math simple example
static Result<int> Divide(int a,int b)=>b==0?RezErr.DivideByZero:Result.Ok(a/b);
//C# will not overflow unless using checked math
static Result<int> Add(int a, int b)
{
try { return Result.Ok(checked(a + b)); } catch (System.OverflowException) { return RezErr.Overflow; }
}
static Result<int> AddDivide(int a,int b,int c)=> Add(a,b).Bind(r=>Divide(r,c));
The key thing to recognise is that this code will not throw exceptions yet will performed checked maths.
Indeed the primary requirement for this design style is that 'All methods returning Result<T> do not throw exceptions'.
Once this requirement is in place we can remove all error handling of exceptions when calling these methods and simply handle the end case result no matter how long the function call chain.
Transitioning to returning Result<T> from functions is simple to implement and peppers your code with ability to chain function calls using the map and bind.
Once using this mechanism with chained function calls your ability to reason about code and follow the code path is enhanced.
Scott Wlaschin describes this as moving 'up' into a new parallel world which is a nice concept.
What! No Option or Maybe.
This library contains no option or maybe. This is a design decision to enforce transition to use of nullable reference types.
Nullable reference types in effect make the option or maybe types irrelevant. Of course this requires implementing nullable reference types correctly
Well... Not Really.
This library only requires you to master a simple paradigm.
-
Return Result<T> type from your functions.
-
Enforce the rule - 'no exceptions' thrown from these methods.
-
Use Map, Bind, Combine, Tee and their async counter-parts to combine your function calls in call chains.
Action and return in C#
Experience over the last year indicated scenarios where this method to combine results into a tuple to be extremely useful.
Eg when T1 and T2 were parameters to a subsequent chained method call.
This avoids the need for paramaterised variations of Map and Bind.
How to indicate failure pathway? There are many other 'functional' libraries with their own methodologies and no standardized way.
This library uses a simple construct that can encapsulate a string and / or an exception.
You can create your own class for Error handling easily - just derive from the abstract RezErrorBase class.
To return an error, simply return RezErrBase eg RezErr.OverThrow which is implicitly converted to the appropriate Result<T>
Another decision that was made - the Error property is not wrapped with boolean IsFailure etc
This enables compiler checks which result in safer code. The compiler cant help you if you obfuscate the Error from the compiler.
if(r.Error is null)
{
... work on r.Value
}
if(r.Error is {})
{
string err = r.Error.ToString();//Compiler sees Error not null 😀😀
}
Transitions from value to Result
var r = Result.Ok("Hello World");
Transitions from RezErrorBase to Result
Result<int> r = RezErr.OverFlow; //Implicit conversion to Result<int>
Transitions via Func<T1,T2> to Result<T2>
Result<int> r = Result.Ok(1).Map(i=>i+1);
Transitions via Func<T1,Result<T2>> to Result<T2>
Result<int> r = Result.Ok(1).Bind(i=>Result.Ok(i+1));
Transition from sync to async
public async Task<Result<string>> GetWebPage(Uri) {.......}
public Result<Uri> GetUri(string url) { ......}
public Result<int> ProcessPage(string content) { ......}
//within an async method
var r = GetUri(uri)//in the world of Result now
.BindAsync(GetWebPage) //in the world of async Result now
.BindAsync(ProcessPage);//remain in the async Result world
//Question what is r
//Answer : Task<Result<string>>
//no async code has executed - page not retrieved and not processed - C# has become Lazy
var finalresult = await r;
Complex examples from production code
// Azure Function Demonstrates a chain of predictable code that does not stray from functional principles and is predictable, easy to read and easy to reason about
// No complex decision branching - just one path
// Intrinsic error Handling
public static class Retriever {
[FunctionName("Retrieve")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Retrieve/{cid}/{itemid}")] HttpRequest req
, string cid
, string itemid
, ILogger log) {
log = new LoggerWrap(log);
var rez = await req
.FailBadRequest(string.IsNullOrEmpty(itemid) || string.IsNullOrEmpty(cid))
.Bind(r => r.VerifyRSAHandshakeResult())
.Bind(_ => CosmosDB.GetStreamService(cid))
.MapAsync(c=> c.ReadItemStreamAsync(itemid))
.BindAsync(crm => crm.ToRsaStreamResult());
return rez.Terminate(log);
}
IAsyncEnumerable
// IAsyncEnumerable available from DotNet Framework 4.5 with Dasync.Collections
private IAsyncEnumerable<Result<CardIdInfo>> GetCardInfos(Campaign campaign)
{
return campaign
.Query<Campaign.CampaignCard>()
.ListAsync() //IAsyncEnumerable<CampaignCard>
.BindAsync(Convert);
static async Task<Result<CardIdInfo>> Convert(CampaignCard card)//local static async function
{
return await RestV2
.Restutil
.PostAsync<CardInfo>(CardFunctionUrls.CardInfoFunction(), card.CardJson)
.MapAsync(ci => new CardIdInfo(card.id, ci));
}
}
Discriminated Unions are explained in Scotts Functional Designs talks on Domain modelling
This library includes a discriminated Union (DUnion) type
Conceptually, Discriminated Unions are a sum type (include the sum of the set of each type).
Concretely, Discriminated union is evaluated as either of its included types.
Option<int> is a type that includes the set sum of the set of integers and None
DUnion<int,bool> is a type that includes the set sum of the set of integers and the set of strings.
The common way of combining types in C# classes, tuples etc is the perumtative or combination of types.
A Tuple<int,bool> is an example of creating new type by “multiplying” existing types together and its set is the combination set of all integers and all bools.
[TestMethod]
public void Payment_Model() //# Scott Wlaschin Domain modelling https://youtu.be/PLFl95c-IiU?t=1169
{
Result<Payment> payment = Cash.Create(42)
.Map(c => new PaymentMethod(c))
.Combine(pm => PaymentAmount.Create(42))
.Map(tup => new Payment() { Amount = tup.Item2, Method = tup.Item1, Currency = Currency.USD });
}
//# Model classes
class MasterCard
{
public CardNumber CardNumber { get; set; }
}
class Visacard
{
public CardNumber CardNumber { get; set; }
}
class Cheque
{
public CheckNumber CheckNumber { get; set; }
}
class CreditCardInfo : DUnion2<MasterCard, Visacard>//? Discriminated Union 'Or Type'
{
public CreditCardInfo(MasterCard mc) : base(mc) { }
public CreditCardInfo(Visacard visa) : base(visa) { }
}
class PaymentMethod : DUnion3<Cash, Cheque, CreditCardInfo>//? Discriminated Union 'Or Type'
{
public PaymentMethod(CreditCardInfo card) : base(card) { }
public PaymentMethod(Cheque cheque) : base(cheque) { }
public PaymentMethod(Cash cash) : base(cash) { }
}
class Payment
{
public PaymentAmount Amount { get; set; }
public Currency Currency { get; set; }
public PaymentMethod Method { get; set; }
}
Scott does a nice example for a card game design in F#
It is possible to do similar in less lines of code in c# This is runnable - hence the extra lines of code - not just a design see Domain_Modelling_CardGame_Scott_Wlaschin.cs
using Card = System.ValueTuple<Rank, Suit>
static List<Card> OpenNewDeckOfCards()
{
var deck = new List<Card>();
//Fill Deck
for (Suit i = Suit.Heart; i <= Suit.Club; i++)
{
for (Rank j = Rank.Two; j <= Rank.Ace; j++)
{
deck.Add((j,i));
}
}
return deck;
}
static ImmutableStack<Card> Shuffle(List<Card> deck)
{
var array = deck.ToArray();
Random rng = new Random();
int n = array.Length;
while (n > 1)
{
int k = rng.Next(n);
n--;
Card temp = array[n];
array[n] = array[k];
array[k] = temp;
}
var stack = ImmutableStack<Card>.Empty;
foreach (var item in array)
{
stack = stack.Push(item);
}
return stack;
}
static void ShowHand((string name, ImmutableList<Card> hand) player)
{
Debug.WriteLine($"Player '{player.name}' has these cards.");
foreach (var item in player.hand.OrderBy(cc => cc.Item1).ThenBy(c2 => c2.Item2))
{
Debug.WriteLine(item);
}
foreach (var g in player.hand.GroupBy(crd => crd.Item1).Where(z => z.Count() > 1))
{
Debug.WriteLine($"Player '{player.name}' has {g.Count()} {g.Key}'s. What a WINNER!");
}
}
List<Card> deck = OpenNewDeckOfCards();//? 3
ImmutableStack<Card> shuffledDeck = Shuffle(deck);
var player = (name: "Hard Luck", hand: ImmutableList<Card>.Empty);
for (int i = 0; i < 5; i++)//? 6
{
shuffledDeck = shuffledDeck.Pop(out Card card); //deal
player.hand = player.hand.Add(card);//pickup
}
ShowHand(player);
Functional Design Patterns - Scott Wlaschin The original.
Railway-Oriented Programming in C# - Marcus Denny
The Power of Composition - Scott Wlaschin