This second edition of the book was written for C# 10 and takes advantage of the language’s latest features, given that they’re relevant to FP. If you’re working on legacy projects that use previous versions of C#, you can still apply all of the ideas discussed in this book. This appendix shows you how.
In the book, I’ve used records and structs for all data objects. Records are immutable by default, and structs are copied by value when passed between functions so that they too are perceived as being immutable. If you want to work with immutable data objects but need to use a version prior to C# 9, you have to rely on one of the following options:
To illustrate each of these strategies, I’ll go back to the task of writing an AccountState
class to represent the state of a bank account in the BOC application. We saw this in section 11.3.
Before the introduction of records, C# developers usually defined data objects with an empty constructor and property getters and setters. The following listing shows how you could model the state of a bank account using this approach.
public enum AccountStatus { Requested, Active, Frozen, Dormant, Closed } public class AccountState { public AccountStatus Status { get; set; } public CurrencyCode Currency { get; set; } public decimal AllowedOverdraft { get; set; } public List<Transaction> TransactionHistory { get; set; } public AccountState() => TransactionHistory = new List<Transaction>(); }
This allows us to create new instances elegantly with the object initializer syntax as in the following listing.
This creates a new account with the Currency
property set explicitly; other properties are initialized to their default values. Note that the object initializer syntax calls the parameterless constructor and the public setters defined in AccountState
.
If we want to represent a change in state, such as if the account is frozen, we’ll create a new AccountState
with the new Status
. We can do this by adding a convenience method on AccountState
as the following listing shows.
public class AccountState { public AccountState WithStatus(AccountStatus newStatus) => new AccountState { Status = newStatus, ❶ Currency = this.Currency, ❷ AllowedOverdraft = this.AllowedOverdraft, ❷ TransactionHistory = this.TransactionHistory ❷ }; }
❷ All other fields are copied from the current state.
WithStatus
is a method that returns a copy of the instance, identical to the original in everything except the Status
, which is as given. This is similar to the behavior you get with AddDays
and similar methods defined on DateTime
: they all return a new instance (see section 11.2.1).
Methods like WithStatus
are called copy methods or with-ers because the convention is to name them With[Property]
. The following listing shows an example of calling a copy method to represent a change in the state of the account.
Copy methods are similar to with
expressions in records, in that they return a copy of the original object, where one property has been updated.
NOTE The cost of representing changes through copy methods is not as high as you might think, as already discussed in section 11.3 (specifically in the sidebar on “Performance impact of using immutable objects”). This is because a copy method like WithStatus
creates a shallow copy of the original: an operation that is fast and sufficient to guarantee safety (assuming that all the object’s children are immutable as well).
The implementation shown so far uses property setters to initially populate an object (section A.1.1) and copy methods to obtain updated versions (section A.1.2). This approach is called immutability by convention: you use convention and discipline to avoid mutation. The setters are exposed, but they should never be called after the object has been initialized. But this doesn’t prevent a mischievous colleague who’s not sold on immutability from setting the fields directly:
If you want to prevent such destructive updates, you’ll have to make your object immutable by removing property setters altogether. New instances must then be populated by passing all values as arguments to the constructor as the following listing shows.
public class AccountState { public AccountStatus Status { get; } public CurrencyCode Currency { get; } public decimal AllowedOverdraft { get; } public List<Transaction> Transactions { get; } public AccountState ( CurrencyCode Currency, AccountStatus Status = AccountStatus.Requested, decimal AllowedOverdraft = 0, List<Transaction> Transactions = null ) { this.Status = Status; this.Currency = Currency; this.AllowedOverdraft = AllowedOverdraft; this.Transactions = Transactions ?? new List<Transaction>(); } public AccountState WithStatus(AccountStatus newStatus) => new AccountState ( Status: newStatus, Currency: this.Currency, AllowedOverdraft: this.AllowedOverdraft, Transactions: this.TransactionHistory ); }
In the constructor, I’ve used named parameters and default values in such a way that I can create a new instance with a syntax that is similar to the object initializer syntax we were using before. We can now create a new account with sensible values like this:
The WithStatus
copy method works just like before. Notice that we’ve now enforced that a value must be provided for Currency
, which isn’t possible when you use the object initializer syntax. So we’ve kept readability while making the implementation more robust.
TIP Forcing the clients of your code to use a constructor or a factory function to instantiate an object improves the robustness of your code because you can enforce business rules at this point, making it impossible to create an object in an invalid state, such as an account with no currency.
We’re still not done because for an object to be immutable, all its constituents must be immutable too. Here we’re using a mutable List
, so your mischievous colleague could still effectively mutate the account state by writing this:
The most effective way to prevent this is to create a copy of the list given to the constructor and store its contents in an immutable list. The following listing shows how to do this using the ImmutableList
type in the System.Collections.Immutable
library.1
using System.Collections.Immutable; public sealed class AccountState ❶ { public IEnumerable<Transaction> TransactionHistory { get; } public AccountState(CurrencyCode Currency , AccountStatus Status = AccountStatus.Requested , decimal AllowedOverdraft = 0 , IEnumerable<Transaction> Transactions = null) { // ... TransactionHistory = ImmutableList.CreateRange ❷ (Transactions ?? Enumerable.Empty<Transaction>()); ❷ } }
❶ Marks the class as sealed to prevent mutable subclasses
❷ Creates and stores a defensive copy of the given list
When a new AccountState
is created, the given list of transactions is copied and stored in an ImmutableList
. This is called a defensive copy. Now the list of transactions of an AccountState
can’t be altered by any consumers, and it remains unaffected even if the list given in the constructor is altered at a later point. Fortunately, CreateRange
is smart enough that if it’s given an ImmutableList
, it just returns it so that copy methods won’t incur any additional overhead.
Furthermore, Transaction
and Currency
must also be immutable types. I’ve also marked AccountState
as sealed
to prevent the creation of mutable subclasses. Now AccountState
is truly immutable, at least in theory. In practice, one could still mutate an instance using reflection so that your mischievous colleague can still have the upper hand.2 But at least now there’s no room for mutating the object by mistake.
How can you add a new transaction to the list? You don’t. You create a new list that has the new transaction as well as all existing ones and that will be part of a new AccountState
, as the following listing demonstrates.
using LaYumba.Functional; ❶ public sealed class AccountState { public AccountState Add(Transaction t) => new AccountState ( Transactions: TransactionHistory.Prepend(t), ❷ Currency: this.Currency, ❸ Status: this.Status, ❸ AllowedOverdraft: this.AllowedOverdraft ❸ ); }
❶ Includes Prepend
as an extension method on IEnumerable
❷ A new IEnumerable
including existing values and the one being added
❸ All other fields are copied as usual.
Notice that in this particular case, we’re prepending the transaction to the list. This is domain-specific; in most cases, you’re interested in the latest transactions, so it’s most efficient to keep the latest ones at the front of the list.
Now that we’ve managed to properly implement AccountState
as an immutable type, let’s face one of the pain points: writing copy methods is no fun! Imagine an object with 10 properties, all of which need copy methods. If there are any collections, you’ll need to copy them into immutable collections and add copy methods that add or remove items from those collections. That’s a lot of boilerplate!
The following listing shows how to mitigate this by including a single With
method with named optional arguments, much like how we used them in the AccountState
constructor in listing A.5.
public AccountState With ( AccountStatus? Status = null, ❶ decimal? AllowedOverdraft = null ❶ ) => new AccountState ( Status: Status ?? this.Status, ❷ AllowedOverdraft: AllowedOverdraft ?? this.AllowedOverdraft, ❷ Currency: this.Currency, ❸ Transactions: this.TransactionHistory ❸ );
❶ null
indicates that the field isn’t specified.
❷ If no value is specified, uses the current instance’s value
❸ You can prevent arbitrary changes.
The default value of null
indicates that the value hasn’t been specified; in which case, the current instance’s value is used to populate the copy. For value-type fields, you can use the corresponding nullable type for the argument type to allow a default of null
. Because the default value null
indicates that the field hasn’t been specified, and hence the current value will be used, it’s not possible to use this method to set a field to null
. Given the discussion on null
versus Option
in section 5.5.1, you can probably see that this isn’t a good idea anyway.
Notice that in listing A.8, we’re only allowing changes to two fields because we’re assuming that we can never change the currency of a bank account or make arbitrary changes to the transaction history. This approach allows us to reduce boilerplate while still retaining fine-grained control over what operations we want to allow. The usage is as follows:
public static AccountState Freeze(this AccountState account) => account.With(Status: AccountStatus.Frozen); public static AccountState RedFlag(this AccountState account) => account.With ( Status: AccountStatus.Frozen, AllowedOverdraft: 0m );
This not only reads clearly but gives us better performance compared to using the classic With[Property]
methods: if we need to update multiple fields, a single new instance is created. I definitely recommend using this single With
method over defining a copy method for every field.
Another approach is to define a generic helper that does the copying and updating without the need for any boilerplate. I’ve implemented such a general-purpose With
method in the LaYumba.Functional.Immutable
class, and it can be used as the following listing shows.
using LaYumba.Functional; var oldState = new AccountState("EUR", AccountStatus.Active); var newState = oldState.With(a => a.Status, AccountStatus.Frozen); oldState.Status // => AccountStatus.Active newState.Status // => AccountStatus.Frozen newState.Currency // => "EUR"
Here, With
is an extension method on object
that takes an Expression
identifying the property to be updated and the new value. Using reflection, it then creates a bitwise copy of the original object, identifies the backing field of the specified property, and sets it to the given value.
In short, it does what we want—for any field and any type. On the upside, this saves us from having to write tedious copy methods. On the downside, reflection is relatively slow, and we lose the fine-grained control available when we explicitly choose what fields can be updated in With
.
In summary, before the introduction of records in C# 9, enforcing immutability was a thorny business, and one of the biggest hurdles when programming functionally.
Here are the pros and cons of the two approaches I've discussed:
Immutability by convention—In this approach, you don’t do any extra work to prevent mutation; you just avoid it like you probably avoid the use of goto
, unsafe
pointer access, and bitwise operations (just to mention a few things that the language allows but that have proven problematic). This can be a viable choice if you’re working independently or with a team that’s sold on this approach from day one. The downside is, of course, that mutation can creep in.
Define immutable objects in C#—This approach gives you a more robust model that communicates to other developers that the object shouldn’t be mutated. It is preferable if you’re working on a project where immutability isn’t used across the board. Compared to immutability by convention, it requires at least some extra work in defining constructors.
To make things even more complicated, third-party libraries may have limitations that dictate your choices. Traditionally, deserializers and ORMs for .NET have used the empty constructor and settable properties to create and populate objects. If you’re relying on libraries with such requirements, immutability by convention may be your only option.
Pattern matching is a language feature that allows you to execute different code depending on the shape of some data—most importantly, its type. It’s a staple of statically typed functional languages, and we’ve used it extensively in the book, whether through switch
expressions or through the definition of a Match
method.
In this section, I’ll describe how support for pattern matching has evolved through successive versions in C# and show you a solution to use pattern matching even if you’re working on an older version of C#.
For a long time, C# had poor support for pattern matching. Until C# 7, the switch
statement only supported a very limited form of pattern matching, allowing you to match on the exact value of an expression. What about matching on the type of an expression? For example, suppose you have the following simple domain:
enum Ripeness { Green, Yellow, Brown } abstract class Reward { } class Peanut : Reward { } class Banana : Reward { public Ripeness Ripeness; }
Up to C# 6, computing a description of a given Reward
had to be done as the following listing shows.
string Describe(Reward reward) { Peanut peanut = reward as Peanut; if (peanut != null) return "It's a peanut"; Banana banana = reward as Banana; if (banana != null) return $"It's a {banana.Ripeness} banana"; return "It's a reward I don't know or care about"; }
For such a simple operation, this is incredibly tedious and noisy. C# 7 introduced some limited support for pattern matching so that the preceding code could be abridged as the next listing shows.
string Describe(Reward reward) { if (reward is Peanut _) return "It's a peanut"; if (reward is Banana banana) return $"It's a {banana.Ripeness} banana"; return "It's a reward I don't know or care about"; }
Or, alternatively, using the switch
statement as the following listing shows.
string Describe(Reward reward) { switch (reward) { case Peanut _: return "It's a peanut"; case Banana banana: return $"It's a {banana.Ripeness} banana"; default: return "It's a reward I don't know or care about"; } }
This is still fairly awkward, especially because in FP, we’d like to use expressions, whereas both if
and switch
expect statements in each branch.
Finally, C# 8 introduced switch
expressions (you saw several examples in the book), allowing us to write the preceding code as the following listing shows.
string Describe(Reward reward) => reward switch { Banana banana => $"It's a {banana.Ripeness} banana", Peanut _ => "It's a peanut", _ => "It's a reward I don't know or care about" };
If you’re working on a codebase that uses a version prior to C# 8, you can still pattern match on type using the Pattern
class I’ve included in LaYumba.Functional
. It can be used as in the following listing.
string Describe(Reward reward) => new Pattern<string> ❶ { (Peanut _) => "It's a peanut", ❷ (Banana b) => $"It's a {b.Ripeness} banana" ❷ } .Default("It's a reward I don't know or care about") ❸ .Match(reward); ❹
❶ The generic parameter specifies the type that’s returned when calling Match
.
❷ A list of functions; the first one with a matching type is evaluated.
❸ Optionally adds a default value or handler
❹ Supplies the value on which to match
This isn’t as performant as first-class language support nor does it have all the bells and whistles like deconstruction, but it’s still a good solution if you’re just interested in matching on type.
You first set up the functions that handle each case (internally, Pattern
is essentially a list of functions, so I’m using list initializer syntax). You can optionally call Default
to provide a default value or a function to use if no matching function is found. Finally, you use Match
to supply the value to match on; this will evaluate the first function whose input type matches the type of the given value.
There’s also a non-generic version of Pattern
in which Match
returns dynamic
. You could use this in the preceding example by simply omitting <string>
, making the syntax a bit cleaner still.
TIP In this book, you saw implementations of a Match
method for Option
, Either
, List
, Tree
, and so on. These effectively perform pattern matching. Defining such methods makes sense when you know from the start all the cases you’ll need to handle (for instance, Option
can only be Some
or None
). By contrast, the Pattern
class is useful for types that are open for inheritance, like Event
or Reward
, where you can envisage adding new subclasses as the system evolves.
To illustrate the techniques described previously, let’s revisit our event sourcing scenario from section 13.2 and suppose we can only use C# 6. We don’t have records, so to represent the state of an account, we’ll define AccountState
as an immutable class. All properties will be read-only and will be populated in the constructor. The following listing shows the implementation.
public sealed class AccountState { public AccountStatus Status { get; } ❶ public CurrencyCode Currency { get; } ❶ public decimal Balance { get; } ❶ public decimal AllowedOverdraft { get; } ❶ public AccountState ( CurrencyCode Currency, AccountStatus Status = AccountStatus.Requested, decimal Balance = 0m, decimal AllowedOverdraft = 0m ) { this.Currency = Currency; ❷ this.Status = Status; ❷ this.Balance = Balance; ❷ this.AllowedOverdraft = AllowedOverdraft; ❷ } public AccountState WithStatus(AccountStatus newStatus) ❸ => new AccountState ( Status: newStatus, Balance: this.Balance, Currency: this.Currency, AllowedOverdraft: this.AllowedOverdraft ); public AccountState Debit(decimal amount) ❸ => Credit(-amount); public AccountState Credit(decimal amount) ❸ => new AccountState ( Balance: this.Balance + amount, Currency: this.Currency, Status: this.Status, AllowedOverdraft: this.AllowedOverdraft ); }
❶ All properties are read-only.
❷ Initializes properties in the constructor
❸ Exposes copy methods for creating modified copies
Other than properties and the constructor, AccountState
has a WithStatus
copy method that creates a new AccountState
with an updated status. Debit
and Credit
are also copy methods that create a copy with an updated balance. (This pretty long class definition replaces the record definition in listing 13.2, which was only seven lines.)
Now, about state transitions. Remember that we use the first event in the account’s history to create an AccountState
and then use each event to compute the account’s new state after the event. The signature for a state transition is
In order to implement the state transition, we pattern match on the type of event and update the AccountState
accordingly:
public static class Account { public static AccountState Create(CreatedAccount evt) ❶ => new AccountState ( Currency: evt.Currency, Status: AccountStatus.Active ); public static AccountState Apply (this AccountState account, Event evt) => new Pattern ❷ { (DepositedCash e) => account.Credit(e.Amount), (DebitedTransfer e) => account.Debit(e.DebitedAmount), (FrozeAccount _) => account.WithStatus(AccountStatus.Frozen), } .Match(evt); }
❶ CreatedAccount
is a special case because there is no prior state.
❷ Calls the relevant transition, depending on the type of the event
Without being able to rely on language support for pattern matching, this code uses the pattern matching solution shown in section A.2.2 to great effect.
As you saw, all the techniques discussed in this book can be used on legacy projects that use older versions of C#. Of course, if you can, do upgrade to the latest version of C# to take advantage of new language features, especially records and pattern matching.
1 The System.Collections.Immutable
library was developed by Microsoft to complement the mutable collections in the BCL, so its feel should be familiar. You must get it from NuGet.
2 The utilities in System.Reflection
allow you to view and modify the value of any field at run time, including private
and readonly
fields and the backing fields of autoproperties.
3.145.167.176