Appendix A. Working with previous version of C#

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.

A.1 Immutable data objects before C# 9

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:

  • Treat objects as immutable by convention.

  • Manually define immutable objects.

  • Use F# for your domain objects.

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.

A.1.1 Immutability by convention

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.

Listing A.1 A simple model for the state of a bank account

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.

Listing A.2 Using the convenient object initializer syntax

var account = new AccountState
{
   Currency = "EUR"
};

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.

A.1.2 Defining copy methods

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.

Listing A.3 Defining a copy method

public class AccountState
{
   public AccountState WithStatus(AccountStatus newStatus)
      => new AccountState
      {
         Status = newStatus,                            
         Currency = this.Currency,                      
         AllowedOverdraft = this.AllowedOverdraft,      
         TransactionHistory = this.TransactionHistory   
      };
}

The updated field

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.

Listing A.4 Obtaining a modified version of the object

var newState = account.WithStatus(AccountStatus.Frozen);

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).

A.1.3 Enforcing immutability

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:

account.Status = AccountStatus.Frozen;

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.

Listing A.5 Refactoring towards immutability: removing all setters

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:

var account = new AccountState
(
   Currency: "EUR",
   Status: AccountStatus.Active
);

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.

A.1.4 Immutable all the way down

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:

account.Transactions.Clear();

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

Listing A.6 Preventing mutation by using immutable collection

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.

Listing A.7 Adding an element to a list requires a new parent object

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.

A.1.5 Copy methods without boilerplate?

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.

Listing A.8 A single With method that can set any property

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.

Listing A.9 Using a general-purpose copy method

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.

A.1.6 Comparing strategies for immutability

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.

A.2 Pattern matching before C# 8

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#.

A.2.1 C#'s incremental support for pattern matching

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.

Listing A.10 Matching on the type of an expression in C# 6

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.

Listing A.11 Matching on type in C# 7 with is

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.

Listing A.12 Matching on type in C# 7 with switch

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.

Listing A.13 A switch-expression in C# 8

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"
   };

A.2.2 A custom solution for pattern matching expressions

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.

Listing A.14 A custom Pattern class for expression-based pattern matching

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.

A.3 Revisiting the event sourcing example

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.

Listing A.15 An immutable class representing the state of an account

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

AccountState  Event  AccountState

In order to implement the state transition, we pattern match on the type of event and update the AccountState accordingly:

Listing A.16 Modeling state transitions with pattern matching

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.

A.4 In conclusion

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.167.176