A practical functional library for C# developers inspired by Scala, Cats, Rust and Kotlin.
FnTools can be found here on NuGet and can be installed with the following command in your Package Manager Console.
Install-Package FnToolsAlternatively if you're using .NET Core then you can install FnTools via the command line interface with the following command:
dotnet add package FnToolsTo 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();Result
Result is the type used for returning and propagating errors. It represents either success (Ok()) or failure (Error()).
It's similar to Try type, but is designed to work with any type of error.
enum MathError
{
DivisionByZero,
NonPositiveLogarithm,
NegativeSquareRoot,
}
Result<decimal, MathError> Div(decimal x, decimal y) =>
Try(() => x / y).ToResult().ErrorMap(_ => MathError.DivisionByZero);
Result<double, MathError> Sqrt(double x) =>
Ok(Math.Sqrt(x)).Filter(x >= 0, MathError.NegativeSquareRoot);
Result<double, MathError> Ln(double x)
{
if (x > 0)
return Ok(Math.Log(x));
else
return Error(MathError.NonPositiveLogarithm);
}
// sqrt(ln(x / y))
Result<double, string> Op(decimal x, decimal y)
{
var result =
from a in Div(x, y).Map(Convert.ToDouble)
from b in Ln(a)
from c in Sqrt(b)
select c;
return result.ErrorMap(ToString<MathError>());
}
Op(1, 0).ShouldBe("DivisionByZero");
Op(1, -10).ShouldBe("NonPositiveLogarithm");
Op(1, 10).ShouldBe("NegativeSquareRoot");
Op(1, 1).ShouldBe(0);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");v0.2.1 (25.04.2020)
- Added Result (thanks @rudewalt !)
- Added FlatTap() for Option, Either and Try
- Added BiMap() for Either
- Documented
- Numerous tiny fixes and improvements
v0.1.14 (13.04.2020)
- Initial release