Skip to content

awsxdr/func

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Func

License: MIT Release CI

Func is a library designed to expand C#'s functional programming capabilities.

Contents

Usage

Map and Tee

Map and Tee are methods used to chain method calls. They can be thought of as equivalent to Select and ForEach but operating on single items.

Map takes in a value, executes a given function on it and returns the result. This is similar to the behaviour of the |> operator in F#.

Tee takes in a value, executes a given method, and returns the original value.

using Func;

public class Example
{
    private readonly ILogger _logger;

    public Example(ILogger logger) => _logger = logger;

    public string GetWelcomeMessage() =>
        GetCurrentUserId()
        .Tee(id => _logger.LogInformation("Generating welcome message for user {0}", id))
        .Map(GetUserNameFromId)
        .Map(GetWelcomeMessageForUserName);

    private int GetCurrentUserId() => 123;
    private string GetUserNameFromId(int id) => "Test User";
    private string GetWelcomeMessageForUserName(string userName) => $"Hello, {userName}";
}

Asynchronous operations

Map and Tee are designed to work happily with asynchronous functions returning Task objects. Methods returning tasks which are chained together will return a single unified task.

using Func;

public class Example
{
    private readonly ILogger _logger;

    public Example(ILogger logger) => _logger = logger;

    public async Task<string> GetWelcomeMessage() =>
        await GetCurrentUserId()
        .Tee((int id) => _logger.LogInformation("Generating welcome message for user {0}", id))
        .Map(GetUserNameFromId)
        .Map(GetWelcomeMessageForUserName);

    private Task<int> GetCurrentUserId() => Task.FromResult(123);
    private Task<string> GetUserNameFromId(int id) => Task.FromResult("Test User");
    private string GetWelcomeMessageForUserName(string userName) => $"Hello, {userName}";
}

Here, several methods are chained together; some of them returning tasks, some not. A single Task is returned which performs all of the chained actions.

Of note here is the use of int on Tee. Generally the compiler will be able to infer the types being used, but in cases like this where the argument is of type object a type needs specifying. If you encounter these sort of errors you can provide the types or, perhaps preferably, provide a more strongly typed method to call.

Multiple arguments

Map and Tee support methods with up to 15 arguments. Additional arguments are provided as parameters to Map and Tee following the method. The value that Map or Tee is acting on is always passed as the last argument.

In the following example, we call string.Join which has the signature string Join(string separator, IEnumerable<string> values). Map is called on the string array and the separator is passed as an argument to Map.

public class Example
{
    public void OutputUserNames() =>
        GetUserNames()
        .Map(string.Join, ", ")
        .Tee(Console.WriteLine);

    private IEnumerable<string> GetUserNames() => new[] { "Anne", "Brian", "Claire", "Daniel" };
}

Option

Options represent a way of marking a value as optional. Optional items offer benefits over nullable items because they enforce checking for a value and thus remove the possibility of ending up with the dreaded NullReferenceExceptions.

using static Func.Option;

public class Example
{
    public static string GetDescription(Option<int> value) =>
        value switch
        {
            Some<int> x when x.Value > 10 => "Huge",
            Some<int> x when x.Value > 5 => "Big",
            Some<int> x when x.Value < 1 => "Tiny",
            Some<int> x when x.Value < 5 => "Small",
            Some<int> _ => "Average",
            None _ => "Empty",
            _ => "Unexpected!"
        };

    public void Test()
    {
        _ = GetDescription(Some(11));    // "Huge"
        _ = GetDescription(Some(3));     // "Small"
        _ = GetDescription(None<int>()); // "Empty"
    }
}

The above example shows a function which expects either an integer or nothing. The Value property can only be retrieved once the Option has been cast to Some.

Also supported is passing values when the type isn't specified. In the below example, any type can be passed in.

using static Func.Option;

public class Example
{
    public static string GetDescription(Option value) =>
        value switch
        {
            Some<int> _ => "Number",
            Some<double> _ => "Number",
            Some<string> _ => "Text",
            Some _ => "Something else",
            None _ => "Empty",
            _ => "Unexpected!"
        };

    public void Test()
    {
        GetDescription(Some(11));      // "Number"
        GetDescription(Some(8.42));    // "Number"
        GetDescription(Some("Hello")); // "Text"
        GetDescription(Some(false));   // "Something else"
        GetDescription(None());        // "Empty"
    }
}

Result

Func supports result types which can be returned from functions to indicate success or failure. Functions which return a Result object can be chained together with calls to Then, And, or Else. If any method in the chain fails then the chain stops executing and a fail is returned. The concept is similar to Javascript's promises or Rust's Result type.

The aim of results is to prevent the use of exceptions for program flow. Methods can fail on non-exceptional errors to avoid continuing without the overhead of throwing an exception.

using System;
using System.Threading.Tasks;
using Func;
using static Func.Result;

public class Example
{
    private readonly ILogger _logger;

    public Example(ILogger logger) => _logger = logger;

    public async Task<string> GetWelcomeMessage(string username) =>
        await
            GetCurrentUserId(username)
            .Then(GetUserFullNameFromId)
            .Then(GetWelcomeMessageForUserFullName)
            .OnSuccess(() => _logger.LogInformation($"User {username} found"))
            .OnError((UserNotFoundError e) => { _logger.LogWarn($"User {username} not found"); })
            .OnError(() => _logger.LogWarn("Something went very wrong!"))
        switch
        {
            Success<string> s => s.Value,
            Failure<UserNotFoundError> _ => "User could not be found",
            Failure _ => throw new Exception("Unexpected error occurred getting welcome message for user")
        };

    private Task<Result<int>> GetCurrentUserId(string username) =>
        (username == "test"
            ? Succeed(123)
            : Result<int>.Fail(new UserNotFoundError())
        ).ToTask();

    private Task<Result<string>> GetUserFullNameFromId(int id) =>
        Succeed("Test User").ToTask();

    private Result<string> GetWelcomeMessageForUserFullName(string userFullName) =>
        Succeed($"Hello, {userFullName}");
}

public class UserNotFoundError : ResultError { }

Note here that the call to Fail requires the result types to be passed. This is because the types can't be inferred.

Also note that the error type is specified in the lambda for OnError. If not specified here then the method would require both the error type and result type specified in angular brackets.

Union

A Union is used to represent a value which can be one of several types. To determine what type is held by the union, either use the Is<> method, or use the is keyword on the Value property.

This has a lot of similarities to Option but constrains the types which are stored.

The following example is to show how unions are used. However, it would likely be better to use overloaded methods for this specific case.

using static Func.Result;

public class Example
{
    public static Result<string> GetDescription(Union<int, double, string> value) =>
        (value.Value switch
        {
            Some<int> x => Succeed(x.Value),
            Some<double> x => Succeed((int)x.Value),
            Some<string> x =>
                int.TryParse(x.Value, out var i)
                    ? Succeed(i)
                    : Result<int>.Fail<FormatError>(),
            _ => throw new Exception("Unexpected type")
        })
        .ThenMap(x =>
            x > 10 ? "Huge"
            : x > 5 ? "Big"
            : x< 1 ? "Tiny"
            : x< 5 ? "Small"
            : "Average"
        );

    public void Test()
    {
        GetDescription(11);      // Success - "Huge"
        GetDescription(8.42);    // Success - "Big"
        GetDescription("2");     // Success - "Small"
        GetDescription("Hello"); // Failure
    }

    public class FormatError : ResultError {}
}