Skip to content

tfg1434/FunctionalSharp

Repository files navigation

FunctionalSharp

Functional types and patterns for C#

FunctionalSharp Vanilla
functional pic low level pic
  • Flat, monadic composition
  • No boilerplate (try/catch, TryParseXXX, ...)
  • try/catch boilerplate
  • Clunky TryParseXXX pattern
functional pic low level pic
  • One line
  • Uses infinite ranges (i.e. generators)
  • Difficult to read
functional pic low level pic
  • List pattern matching
  • Functional style singly linked list
  • Mutates original collection

Setup

Install FunctionalSharp package from nuget or add a reference to the dll in your project.

Then, all you need to do is add

global using FunctionalSharp;
global using static FunctionalSharp.F;
global using Unit = System.ValueTuple;

into a file for each project you want to use this lib in.

Note: If you would like to interact with .NET BCL functionality, install the FunctionalSharp.Wrappers library as well

Documentation

XML docs are provided for most functions and types.

Usage examples and information about key types are provided in Types

Credit

This library was inspired by language-ext by Paul Louth.

Types

Unit

Unit is defined as using Unit = System.ValueTuple;. To return Unit from a method, simply use return Unit().

Unit is advantageous over void because void cannot be used like a real type. Using Unit in place of void lets you do things like interop between Func and Action, or make Linq queries easier. You can easily convert an Action to a Unit-returning Func by using .ToFunc().

In fact, there have even been discussions about adding Unit-like functionality to C#.

Currying & partial application

Note: It's a good idea to order your function signatures from most general -> least general. This makes it easy to partially apply arguments

var greet = (string greeting, string name) => Console.WriteLine($"{greeting}, {name}");

Currying is the process of transforming a function taking multiple arguments into one that takes the arguments one at a time. This makes it easy to reduce code duplication.

var curried = greet.Curry();
curried("Hi")("Bob"); //same as greet("Hi", "Bob");

Partial application is similar to currying, but you can apply the argument directly

var applied = greet.Apply("Hi");
applied("Bob"); //same as greet("Hi", "Bob");

Maybe

Maybe is a discriminated union type with two possible states: Just, and Nothing. Instead of returning null or throwing an exception, consider returning Maybe instead.

Maybe is advantageous over null because the compiler forces you to handle both states. Furthermore, your function signature communicates the purpose better, and the method becomes honest.

To create a Maybe, you can use Just() or Nothing.

Maybe<int> a = Just(1);
Maybe<string> b = Nothing;

Here's an example of making a safe Get function for IDictionary

public static Maybe<V> Get<K, V>(this IDictionary<K, V> map, K key) {
    if (map.TryGetValue(key, out T value))
        return value; //works because of implicit conversions. use Just() if you don't like them
    
    return Nothing;
}

Map

Map is an immutable dictionary implementation, similar to ImmutableSortedDictionary. Under the hood, Map uses an AVL tree for O(log n) search, insert, and delete.

You can easily construct a map by using the Map() factory function or using .ToMap(). All common operations are defined including custom comparers for the AVL tree, Get(), Lookup(), etc.

var map = Map(("a", 1), ("b", 2));
map.Get("b"); //Maybe<int>
map["a"]; //int
map.SetItem("a", 4); //Map<string, int>

Lst

Lst is an implementation of the functional immutable singly linked list. You can easily construct one with List() or ToLst().

Lst includes common functional operations like pattern matching head and tail, prepending, slices, etc.

Unlike .NET BCL List, Lst has structural equality

List(1, 2, 3) == List(1, 2, 3); //true
List(3, 2) + 1 == List(3) + List(2, 1); //true

Error

Error is a record class representing an error that may or may not contain an inner exception. It's similar to Exception, where you can subclass it to make your own more specific errors. Some included errors are found in Errors.cs.

Either

Either is the functional way of error handling.
Instead of throwing exceptions (dishonest methods, disrupt program flow, etc) consider returning Either instead. It's a discriminated union type with two states: Left or Right.

Left represents an error value, while Right represents a success value. Either is different from Maybe because you can add additional information to the fail state.

Example:

public Either<OutOfBoundError, int[]> Slice(int[] arr, int index, int count) {
    if (index < 0) return new($"{nameof(index)} must be non-negative");
    if (count < 0) return new($"{nameof(count)} must be non-negative");
    if (index + count > arr.Length) return new($"({nameof(index)}, {nameof(count)}) must represent a range of indices within the array");
    
    ...
}

Range

Range is a port of Haskell's ranges. Try one out with Range() function, but make sure to use named arguments because some args are optional. The from, second, and to arguments come from Haskell -- [from,second..to].

Because it uses iterators, Range supports infinite ranges, char ranges, etc.

Try

Try is a delegate representing a lazy computation that could throw an exception. After the computation is run, the result is captured in an Exceptional.

As a monad, we can easily compose it using a Linq expression or repeated calls to Bind.

Example of extracting information from a Json string:

Try<double> ParseTemp(string json)
    => from obj in Parse(json)
       let weather = CreateWeather(obj)
       from temp in weather.Map(w => w.Temperature)
       select temp;
       
Try<double> ParseTemp(string json)
    => Parse(json)
           .Bind(obj => CreateWeather(obj).Map(w => w.Temperature));

IO

IO is a monad that lets you interact with real world IO, or any side-effecting computations. Importantly, it lets you do this while keeping your code pure and exceptions compartmentalized. Additionally, IO also contains a type paramater that allows you to pass in your own typeclass (custom environments).

IO is very similar to Haskell's IO monad. Essentially, IO<string> is a wrapper that will perform some action to get a string when invoked. Because it is a monad, you can also chain these computations and lift pure functions.

In order to print a file to the console, let's first import the runtimes for File and Console. File and Console are static classes that emulate the .NET BCL. However, they return IO monads instead of the real types.

using static FunctionalSharp.Wrappers.File<FunctionalSharp.Wrappers.LiveRuntime>;
using static FunctionalSharp.Wrappers.Console<FunctionalSharp.Wrappers.LiveRuntime>;

Next, we need to define the IO-returning function. The signature returns Unit because we are reading the file, printing the contents, then throwing the contents away.

static IO<LiveRuntime, Unit> PrintFile(string path)

Then, let's read the file and print it out. We accomplish this with a Linq query (ReadAllText and WriteAllText return IO monads).

from txt in ReadAllText(path)
from _ in WriteLine(txt)
select Unit();

Finally, let's run the method
Note: In this case we are tossing the result of the operation

PrintFile("myfile.txt").Run(new LiveRuntime());

If we wanted to do an in-between operation with a pure method, we can simply add another let clause to our query or lift a pure function

static IO<LiveRuntime, Unit> PrintFile(string path)
    => from txt in ReadAllText(path)
       let newTxt = txt.Replace("\r\n", "\n")
       from _ in WriteLine(newTxt)
       select Unit();
       
static IO<LiveRuntime, Unit> PrintFile(string path)
    => from txt in ReadAllText(path)
       //we use IOSucc to lift because .Replace is pure
       from newTxt in IOSucc<LiveRuntime, string>(txt.Replace("\r\n", "\n"))
       from _ in WriteLine(newTxt)
       select Unit();

Other types

Documentation for additional types and operations can be found in the source code / xml docs