asc-community / HonkSharp

Some wrappers and API and methods for fast and convenient declarative coding in C#
47 stars 4 forks source link

Honk# logo

Honk#

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.

Go to examples or features.

Functional

Either

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 them

are 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 them

or 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 them

Assume

Either<string, int, (int quack, float duck)> a = ... // we don't care

which is equivalent to F#:

let a : Choice<string, int, {| quack:int; duck:float |}> a = ... // We don't care
let a : (string|int|{| quack:int; duck:float |}) a = ... // We don't care

Here is how we work with Either.

1. Switch over all cases

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}!"

2. Check the type of the either

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}!" | _ -> ()

3. Try casting

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 :("

4. Force casting

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}"

LList

It's a singly-linked immutable list with a head element and tail.

1. Create it from sequence

var list = LList.Of(1, 2, 3);

IEnumerable<string> seq = MyCustomSequence();
var list = LList.Of(seq);

2. Create empty list

var empty = LList.Of<int>();
var empty = LList<int>.Empty;

3. Add elements

var list = LList.Of(1, 2, 3);

var newList = 0 + list; // <=> LList.Of(0, 1, 2, 3)
var newList = list.Add(0); // same

4. Call ToString

var list = LList.Of(1, 2, 3);
Console.WriteLine(list);
>>> [ 1, 2, 3 ]

5. Switch over elements

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!"
};

6. Built-in fluent functions to work with list

var list = LList.Of(1, 2, 3);

list
    .Where(a => a > 2)
    .Map(a => 
        a.ToString()
        .ToArray()
    )
    .Flatten()
    .Reverse();

7. Indexing

var list = LList.Of(1, 2, 3);

list[0] + list[2];

Indexing works slowly (linearly traversing the list).

Fluency

1. Pipe

Method(b)

<=>

b.Pipe(Method)

Equivalent to F#:

Method b

<=>

b |> Method

2. Inject

a
.Inject(b)
.Pipe((a, b) => a + b)

Equivalent to F#:

(a, b)
||> fun a b -> a + b
// or
(a, b) ||> (+)

3. Alias

(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)
||> (+)

4. NullIf

(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 sqrt

5. Let

a
.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,
six

6. LetLazy

a
.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"

7. ReplaceWith

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.

8. Dangerous

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>.

Laziness

Lazy property: what and how?

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

  1. It does NOT affect the comparison. So even if one record has its tripled calculated and the other does not, the value of tripled is not taken into account.
  2. Copying is safe. For example, assume there's some num whose Tripled is already calculated. Then if you copy it with num with { Value = 32 }, Tripled will be calculated again for the new instance.
  3. Is a struct.

A few rules working with LazyProperty:

  1. Do NOT pass it by copy. Otherwise you risk the factory being called more than once.
  2. Do provide this as the argument of GetValue, as it invalidates on the new holder, but is valid as long as the current holder remains.
  3. Do NOT use it on mutable primary properties.

Now, let's consider implementations of this type.

LazyPropertyA

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.

LazyPropertyB

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

Convenient extensions

1. bool.Invert

true.Invert() // returns false

2. string.Parse

Parses numeric types. Returns an either.

"55".Parse<int>()
"5.5".Parse<decimal>()
"23482948294892840928492842424242".Parse<BigInteger>()

3. string.Join

Joins objects over a delimiter.

", ".Join(new [] { 1, 2, 3 }) // returns "1, 2, 3"

4. AsString

Concats a sequence of chars into a string

new [] { 'a', 'b', 'c' }.AsString() // returns "abc"

5. Zip

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

6. Cartesian

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

7. AsRange

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)

8. Enumerate

Allows to go over a sequence keeping the current index of the element.

"abc".Enumerate().ToArray() // returns new [] { (0, 'a'), (1, 'b'), (2, 'c') }

Examples

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"