A practical functional library for C# developers.
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;
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});
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();
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");