/FnTools

A practical functional library for C#

Primary LanguageC#Apache License 2.0Apache-2.0

FnTools

A practical functional library for C# developers.

Build status codecov Nuget

Quickstart

FnTools can be found here on NuGet and can be installed with the following command in your Package Manager Console.

Install-Package FnTools

Alternatively if you're using .NET Core then you can install FnTools via the command line interface with the following command:

dotnet add package FnTools

To have the best experience with FnTools statically import FnTools.Prelude

using static FnTools.Prelude;

Working with functions

FnTools provides different ways of manipulating functions:

Def()

Infers function types

// Doesn't compile
// [CS0815] Cannot assign lambda expression to an implicitly-typed variable
// var id = (int x) => x;

var id = Def((int x) => x);

// Doesn't compile
// [CS0815] Cannot assign method group to an implicitly-typed variable
// var readLine = Console.ReadLine;

var readLine = Def(Console.ReadLine);

Partial()

Does partial application. You can use __ (double underscore) to bypass function arguments.

var version =
    Def((string text, int major, int min, char rev) => $"{text}: {major}.{min}{rev}");

var withMin = version.Partial(__, __, 0);
var versioned = withMin.Partial(__, 2);
var withTextAndVersion = versioned.Partial("Version");
var result = withTextAndVersion('b');

result.ShouldBe("Version: 2.0b");

Compose()

Does function composition

var readInt = Def<string, int>(int.Parse).Compose(Console.ReadLine);

Curry() and Uncurry()

Do currying and uncurrying

var min = Def<int, int, int>(Math.Min);

var minCurry = min.Curry();
minCurry(1)(2).ShouldBe(1);

var minUncurry = minCurry.Uncurry();
minUncurry(1, 2).ShouldBe(Math.Min(1, 2));

Run()

Executes an action or a function immediately

var flag = Run(() => false);
var action = Def(() => { flag = true; });
Run(action);

flag.ShouldBe(true);

Apply()

Applies an action or a function to its caller

(-5)
    .Apply(Math.Abs)
    .Apply(x => Math.Pow(x, 2))
    .Apply(x => Math.Min(x, 30))
    .ShouldBe(25);

// Compare to
// Assert.Equal(25, Math.Min(Math.Pow(Math.Abs(-5), 2), 30));
var sam = new Person {Name = "Sam", Age = 20};
sam
    .Apply((ref Person x) => { x.Age++; })
    .ShouldBe(new Person {Name = "Sam", Age = 21});

Data types

Option

Represents optional values. Instances of Option are either Some() or None.

var input = new[] {"1", "2", "1.7", "Not_A_Number", "3"};

static Option<int> ParseInt(string val) =>
    int.TryParse(val, out var num) ? Some(num) : None;

var result = new StringBuilder().Apply(sb =>
    input
        .Select(ParseInt)
        .ForEach(o => o.Map(sb.Append))
).ToString();

result.ShouldBe("123");

Either

Represents a value of one of two possible types (a disjoint union.) Instances of Either are either Left() or Right().

static Either<string, int> Div(int x, int y)
{
    if (y == 0)
        return Left("cannot divide by 0");
    else
        return Right(x / y);
}

static string PrintResult(Either<string, int> result) =>
    result.Fold(
        left => $"Error: {left}",
        right => right.ToString()
    );

Div(10, 1).Apply(PrintResult).ShouldBe("10");
Div(10, 0).Apply(PrintResult).ShouldBe("Error: cannot divide by 0");

Try

The Try type represents a computation that may either result in an exception (Failure()), or return a successfully computed value (Success()). It's similar to, but semantically different from the Either type.

var tryParse =
    Def((string x) =>
        Try(() => int.Parse(x))
            .Recover<FormatException>(_ => 0)
    );

var trySum =
    from x in tryParse(Console.ReadLine())
    from y in tryParse(Console.ReadLine())
    from z in tryParse(Console.ReadLine())
    select x + y + z;

trySum.IsSuccess.ShouldBe(true);

var sum = trySum.Get();

More samples

var location = new Location {X = 50, Y = 23};
var time = "13:57:59";

Def<string, object[], string>(string.Format)
    .Partial(__, new object[] {location.X, location.Y, time})
    .Apply(LogLocation);

void LogLocation(Func<string, string> log)
{
    log("{2}: {0},{1}").ShouldBe($"{time}: {location.X},{location.Y}");
    log("({0}, {1})").ShouldBe($"({location.X}, {location.Y})");
}
var substring = Def((int start, int length, string str) => str.Substring(start, length));
var firstChars = substring.Partial(0);
var firstChar = firstChars.Partial(1);
var toLower = Def((string str) => str.ToLower());
var lowerFirstChar = toLower.Compose(firstChar);

lowerFirstChar("String").ShouldBe("s");