Modern library for declarative programming in C#. Available on NuGet.
Honk in C#!
We, C# programmers, also deserve beautiful code like in F#. We don't have duck typing, arbitrary operators, discriminated unions, currying, but instead we can have fluent coding, lazy properties, and many more.
This repo contains some wrappers and API and methods for fast and convenient declarative coding in C#, including features from functional, fluent, and lazy programming.
The purpose of this type is to mimic an anonymous DU. For example,
var a = new Either<string, int>(5);
var b = new Either<string, int>("Hello, world");
Either<string, int> f(bool test) => test ? a : b; // We can return either of themare both valid, where in the first one Either is an int, and in the second one, it's a string. It may take up to 16 types.
It is equivalent to F#:
let a = Choice2Of2 5
let b = Choice1Of2 "Hello, world"
let f = function true -> a | false -> b // We can return either of themor F# in the future:
let a = 5
let b = "Hello, world"
let f : _ -> (int|string) = function true -> a | false -> b // We can return either of themAssume
Either<string, int, (int quack, float duck)> a = ... // we don't carewhich is equivalent to F#:
let a : Choice<string, int, {| quack:int; duck:float |}> a = ... // We don't carelet a : (string|int|{| quack:int; duck:float |}) a = ... // We don't careHere is how we work with Either.
var res = a.Switch(
s => $"It's a string {s}!",
i => $"It's an int {i}!",
q => $"It's a tuple {q.quack}!"
)Equivalent to F#:
let res = a |> function
| Choice1Of3 s -> $"It's a string {s}!"
| Choice2Of3 i -> $"It's an int {i}!"
| Choice3Of3 q -> $"It's a tuple {q.quack}!"let res = a |> function
| :? string as s -> $"It's a string {s}!"
| :? int as i -> $"It's an int {i}!"
| :? {| quack:int; duck:float |} as q -> $"It's a tuple {q.quack}!"If you would like to reorder branches, the argument names are case1, case2, case3, etc.
var res = a.Switch(
case2: i => $"It's an int {i}!",
case3: q => $"It's a tuple {q.quack}!",
case1: s => $"It's a string {s}!"
)Equivalent to F#:
let res = a |> function
| Choice2Of3 i -> $"It's an int {i}!"
| Choice3Of3 q -> $"It's a tuple {q.quack}!"
| Choice1Of3 s -> $"It's a string {s}!"let res = a |> function
| :? int as i -> $"It's an int {i}!"
| :? {| quack:int; duck:float |} as q -> $"It's a tuple {q.quack}!"
| :? string as s -> $"It's a string {s}!"if (a.Is<int>(out var i))
Console.WriteLine($"It's an int {i}!");Equivalent to F#:
match a with Choice2Of3 i ->
printfn $"It's an int {i}!" | _ -> ()match a with :? int as i ->
printfn $"It's an int {i}!" | _ -> ()var res = a.As<int>().Switch(
i => $"Cast successful! {i}"
_ => "Cast failed :("
);Equivalent to F#:
let res = a |> function
| Choice2Of3 i -> $"Cast successful! {i}"
| _ -> "Cast failed :("let res = a |> function
| :? int as i -> $"Cast successful! {i}"
| _ -> "Cast failed :("Since As returns an Either of result and failure, we can force the best case by
AssumeBest:
var res = a.As<int>().AssumeBest();
Console.WriteLine($"It's an int: {res}");If a turns out to be a non-int, then AssumeBest will throw an exception (see fluent coding for more info).
Equivalent to F#:
let res = a |> function Choice2Of3 i -> i
printfn $"It's an int: {res}"let res = a |> function :? int as i -> i
printfn $"It's an int: {res}"It's a singly-linked immutable list with a head element and tail.
var list = LList.Of(1, 2, 3);
IEnumerable<string> seq = MyCustomSequence();
var list = LList.Of(seq);var empty = LList.Of<int>();
var empty = LList<int>.Empty;var list = LList.Of(1, 2, 3);
var newList = 0 + list; // <=> LList.Of(0, 1, 2, 3)
var newList = list.Add(0); // samevar list = LList.Of(1, 2, 3);
Console.WriteLine(list);
>>> [ 1, 2, 3 ]var list = LList.Of(1, 2, 3);
var res = list switch
{
LEmpty<int> => "This list is empty!",
(var head, LEmpty<int>) => $"This list has one element: {head}",
(var h1, (var h2, var tail)) => $"This list has at least two elements!11!"
};var list = LList.Of(1, 2, 3);
list
.Where(a => a > 2)
.Map(a =>
a.ToString()
.ToArray()
)
.Flatten()
.Reverse();var list = LList.Of(1, 2, 3);
list[0] + list[2];Indexing works slowly (linearly traversing the list).
Method(b)<=>
b.Pipe(Method)Equivalent to F#:
Method b<=>
b |> Methoda
.Inject(b)
.Pipe((a, b) => a + b)Equivalent to F#:
(a, b)
||> fun a b -> a + b
// or
(a, b) ||> (+)(1 + 2 + 3)
.Alias(out var someVar)
.Pipe(a => a * 2)
.Inject(someVar)
.Pipe((a, b) => a + b)Equivalent to F#:
1 + 2 + 3
|> fun someVar ->
(someVar * 2
, someVar)
||> (+)(a + 2)
.NullIf(a => a < 0)
?.Pipe(a => Math.Sqrt(a))Equivalent to F#:
a + 2.
|> fun a -> if a < 0. then None else Some a
|> Option.map sqrta
.Pipe(a => a + 2)
.Let(out var six, 1 + 2 + 3)
.Pipe(a => a + 4)
.Inject(six)Equivalent to F#:
a
|> (+) 2 |> fun a -> // Can't just let between pipes without aliasing first
let six = 1 + 2 + 3
a + 4,
sixa
.Pipe(a => a + 2)
.LetLazy(out var big, _ => Console.ReadLine().Parse<int>().AssumeBest())
.Pipe(a => a - 1)
.Inject(big)
.Pipe((a, big) => a switch
{
> 0 => 4,
-3 => big
_ => big + a
})
.Execute(Console.WriteLine)Equivalent to F#:
a
|> (+) 2 |> fun a ->
let big = lazy (Console.ReadLine() |> int)
(a - 1
, big)
||> function
| a when a > 0 -> fun _ -> 4
| -3 -> (|Lazy|)
| a -> (|Lazy|) >> (+) a
|> printfn "%d"This is a very simple thing that replaces the current flow end with another object.
public static int SomeMethod()
=> "quack".ReplaceWith(5);The method returns 5.
This creates a block of code which can throw. Try returns an Either of result and failure.
"55".Dangerous().Try<FormatException, int>(int.Parse)returns 55 in an either, but
"quack".Dangerous().Try<FormatException, int>(int.Parse)would return a Failure<FormatException>.
Detailed article is here, but let's go over the main points.
This type serves as a field inside your immutable records to represent a record's secondary property that is only dependent on its primary properties.
Primary properties are those init-able (we don't consider setters in any way here). Secondary
properties are some results, consequences of the primary properties. For example, if we were to have
a type Integer, then its primary property is its int value, but its secondary property is its
Tripled value (since each integer has it), which only depends on its int value.
So to achieve that, we write
public sealed record Number(int Value)
{
... other code
public Number Tripled => tripled.GetValue(...);
private LazyProperty<Number> tripled = ...;
}As you can see, the syntax is very concise. Other benefits is that
- It does NOT affect the comparison. So even if one record has its
tripledcalculated and the other does not, the value oftripledis not taken into account. - Copying is safe. For example, assume there's some
numwhoseTripledis already calculated. Then if you copy it withnum with { Value = 32 },Tripledwill be calculated again for the new instance. - Is a struct.
A few rules working with LazyProperty:
- Do NOT pass it by copy. Otherwise you risk the factory being called more than once.
- Do provide
thisas the argument ofGetValue, as it invalidates on the new holder, but is valid as long as the current holder remains. - Do NOT use it on mutable primary properties.
Now, let's consider implementations of this type.
A stands for after: you pay a bit more time on accessing an instance of LazyPropertyA (after creating your record) but do not
pay extra time before (on creating the instance).
The syntax:
public sealed record Number(int Value)
{
... other code
public Number Tripled => tripled.GetValue(@this => new(@this.Value * 3), this);
private LazyProperty<Number> tripled;
}So as you can see, creating tripled is absolutely free.
B stands for before: you pay a bit more time on creating an instance of LazyPropertyB, but accessing it is a bit faster.
The syntax:
public sealed record Number(int Value)
{
... other code
public Number Tripled => tripled.GetValue(this);
private LazyProperty<Number> tripled = new(@this => new(@this.Value * 3));
}The difference between the two is the time when you pass the factory (on creating your type or on accessing the property).
true.Invert() // returns falseParses numeric types. Returns an either.
"55".Parse<int>()
"5.5".Parse<decimal>()
"23482948294892840928492842424242".Parse<BigInteger>()Joins objects over a delimiter.
", ".Join(new [] { 1, 2, 3 }) // returns "1, 2, 3"Concats a sequence of chars into a string
new [] { 'a', 'b', 'c' }.AsString() // returns "abc"Given a tuple of sequences, zips them into one:
(new [] { 'a', 'b', 'c' }, new [] { 1, 2, 3 }).Zip().ToArray() // returns new [] { ('a', 1), ('b', 2), ('c', 3) }Given a tuple of sequences, finds their cartesian product (aka 'each for each'):
(new [] { 'a', 'b' }, new [] { 1, 2 }).Zip().ToArray() // returns new [] { ('a', 1), ('a', 2), ('b', 1), ('b', 2) }Converts a Range into a sequence of natural numbers.
(2..4).AsRange().ToArray() // returns new [] { 2, 3, 4 }Another Range-related feature is extended Enumerator:
foreach (var i in 2..5)
Console.Write(i);(outputs 2345)
Allows to go over a sequence keeping the current index of the element.
"abc".Enumerate().ToArray() // returns new [] { (0, 'a'), (1, 'b'), (2, 'c') }Imperative C#:
using System;
var input = Console.ReadLine();
string output;
if (int.TryParse(input, out var valid))
output = $"Valid number! {valid}";
else
output = "Oops, invalid!";
Console.WriteLine($"Input: {input}\nOutput: {output}");Declarative C#:
using System;
Console.ReadLine().Alias(out var input)
.Parse<int>()
.Match(valid => $"Valid number! {valid}",
() => "Oops, invalid!")
.Inject(input)
.Pipe((output, input) => $"Input: {input}\nOutput: {output}")
.Execute(Console.WriteLine);F# equivalent of imperative:
open System
let input = Console.ReadLine()
let mutable output = Unchecked.defaultof<_>
let mutable valid = Unchecked.defaultof<_>
if Int32.TryParse(input, &valid) then
output <- $"Valid number! {valid}";
else
output <- "Oops, invalid!";
printfn $"Input: {input}\nOutput: {output}"F# equivalent of declarative:
open System
Console.ReadLine() |> fun input ->
(input |> Int32.TryParse
|> function true, valid -> $"Valid number! {valid}"
| _ -> "Oops, invalid!"
, input)
||> fun output input -> $"Input: {input}\nOutput: {output}"
|> printfn "%s"