Record -> Product of values

A Record type represents a product of set values. It is a similar concept to the ValueTuple type but it provides additional features and its inner values are immutable. It is a value type (struct) and therefore can’t be null.

The ValueTuple type is quite useful in situations when two distinct objects need to be part of the same structure without the need for creating a dedicated type. However, since its inner values are mutable and since it’s quite tedious to transform one ValueTuple object to another one with the same arity, Funk provides a Record type to compensate for these flaws. A Record type is also a functor and a monad as it provides the corresponding mapping and binding functions (see the Maybe type for an explanation of these concepts). Funk is trying to encourage the correct code design, so it only provides the Record type up to an arity of 5 (Record<T1,..,T5>). If you need more than that, it is probably time to rethink your design.

Lifting functions

There are a few explicit ways of creating a Record object.

var recordOf2 = Record.Create("John Doe", 30); // using a factory method -> Record<string, int>
var recordOf3 = ("Jane", "Doe", 30).ToRecord(); // using an extension method -> Record<string, string, int>
var recordOf1 = rec(customers.Get(id)); // using a Prelude function -> Record<Customer>

In the first line, we are creating a Record object from two independent objects (we could also create it from the ValueTuple). In the second line, we create it from the ValueTuple using an extension method. The third one is the simplest and it can be really useful to easily replace all the ValueTuple objects in our code. To use the rec function, you need to import the Prelude as a static reference.

There is an implicit conversion between a ValueTuple and a Record of the same arity so the following code is legal.

public static Record<string, int> GetRecord((string, int) item) => item;

Deconstruction

Same as with the ValueTuple, you can deconstruct the Record object. When we are working with the same type of underlying items of the Record, it can be really helpful to be able to deconstruct the object to avoid the potential mistake and still manage to keep the code size intact. So instead of assigning each underlying item to a variable one by one, we can do it as shown in the following example.

var (name, surname) = GetNameWithSurname(id); // Record<string, string>

Immutability

Record’s inner values are immutable. From the following code we can see that the attempt of changing the value of the Record’s inner item results in the compile-time error.

var record = GetRecord(("John", 30));
var name = record.Item1; // "John"

record.Item1 = "Jane"; // compile-time error

As we see from the example, the Record has the same naming of its inner values as the ValueTuple. However, as opposed to the ValueTuple, it is not possible to change the inner value of the Record object.

We have to be careful here though as if the inner value of the Record object happens to be a reference type or a value type with inner properties, then modifying that object will result in the modified inner value of that Record. So this immutability is here actually to prevent the direct attempt of modification.

Immutability is great but it comes with certain costs and the biggest one is the tedious transformation process. Since we cannot modify the inner value directly, to do so, we need to create a new object. This is especially painful when we work with Record objects of larger arity. Because of that, the Record type provides mapping and binding functions that can help abstract away this issue.

Pattern-matching

One of the ways for working with the Record type is using the pattern-matching approach. Match functions provided allow you to extract the underlying values and transform them into a specified result.

var john = rec("John", "Doe", 30);
var concatenated = john.Match(
    (name, surname, age) => $"{name} {surname} is {age} years old."
);

The Match function provides a fluent way of extracting all the inner values of the specific Record object and uses them in the provided function. It also works with the Action type delegates.

Functor

The Record type provides the Map function to easily transform one Record object to another of the same arity. We could have two related accounts, from which we would like to retrieve corresponding contracts.

public Record<Account, Account> GetAccountWithSubAccount(Guid id) => /* implementation */;

var account = GetAccountWithSubAccount(id);
var (accountContract, subAccountContract) = account.Map((a, s) =>
    (accounts.GetContract(a.ContractId), accounts.GetContract(s.ContractId))
); // Record<Contract, Contract>

Here, we are mapping the underlying values and retrieving the ValueTuple object in the specified function. The Map function is automatically converting it to the Record object which we are later deconstructing. We managed to express this operation without the need for using statements.

There is also an async version of the Map function.

Monad

The Record type also provides the FlatMap function in case we need to flatten the result to avoid object nesting. When the function provided as an argument inside the Map function returns the Record object instead of the ValueTuple, we need to use the FlatMap function instead.

var (registrationContract, accountContract) = await customers.GetWithAccountAsync(id).FlatMapAsync(async (c, a) =>
    rec
    (
        await customers.GetContract(c.ContractId),
        await accounts.GetContract(a.ContractId)
    )
);

Here, the GetWithAccountAsync function is returning the Task<Record<Customer, Account>>. We are then using the async version of the FlatMap to execute this operation asynchronously.