In this post I dig deeper into validation of a Domain-Driven Design with F# and the functional paradigm. The implementation builds upon the design introduced earlier. The central theme is that of explicitness - the possibility of a failure is made explicit with type signatures. In turn, this calls for explicit handling of failure conditions paving the way for a design-by-contract style of programming and equational reasoning. Under the hood, the implementation draws on basic principles of category theory which provide for uniformity and composability. As a result, from the perspective of DDD, the confluence of declarative design, supple design, invariants and assertions is tremendously enhanced. The functional implementation of validation is to a great extent based on the wonderful work of Mauricio Scheffer.
Validation can be a tough topic. In modern object-oriented languages such as C# and Java, a notable reason for the difficulty is the impedance mismatch between exceptions, which are used to handle various types of error conditions, and methods which return normally. The aftermath is a nonuniform interface coupled with reduced autogenesis. In addition to a labyrinthine implementation, usually requiring explicit support from the hosting runtime, exceptions seem to almost invariably lead to bad programming practices. Contrary to their ambition, exceptions compel programmers to defer and ultimately avoid explicit handling of errors. This is because exceptions are highly optimized for the non-degenerate case and despite guidelines and best practices, their fundamental flaws cannot be escaped.
The following is a typical example used to showcase exception deficiencies.
public int Divide(int x, int y)
{
if (y == 0)
throw new ArgumentOutOfRangeException();
return x / y;
}
public void DoSomething()
{
try
{
var x = 1;
var y = 0;
// this is the interface for obtaining the normal result type
var r = Divide(x, y);
Console.WriteLine("Result: {0}", r);
}
// this is the interface for obtaining the erroneuous result type
catch (ArgumentOutOfRangeException)
{
Console.WriteLine("Error");
throw;
}
}
The method interface is not uniform because the result can either be a integer value, which can be captured by assigning the result to a variable, or an exception which can only be captured by a catch block. Furthermore, static verification extends only to the non-exceptional case. In other words, the compiler will issue a warning if one tries to assign the result of this method to a string, but it won’t issue a warning if the result is assigned to an integer variable. This is problematic because the integer type is a proper subset of the method’s effective range - which is a union of the set of integers and an error case.
Unfortunately, the flaws of exceptions extend beyond interface irregularities to friction in designing exception hierarchies as evidenced by the likes of ApplicationException in .NET. Moreover, it isn’t immediately clear whether a given exception should ever be caught. In .NET, it is just as easy to catch an ArgumentException as it is an OutOfMemoryException though it never makes sense to catch the latter since there is not way to handle it. If all of this wasn’t enough, catching exceptions also incurs a performance penalty.
For more about the pitfalls of exceptions take a look at Exceptions are Bad.
The functional programming paradigm addresses the afore-mentioned scenario with explicit typing. Since the range of the Divide function is extended by the possibility of error, the range type is explicitly amplified. One such amplified type in F# is the option type use of which results in the following.
let divide x y = if y = 0 then None else Some (x / y)
let doSomething () =
let x = 1
let y = 0
let r = divide x y
match r with
| Some r -> printfn "Result: %d" r
| None -> printfn "Error"
In this example, an erroneous result is explicitly encoded as an empty option. Therefore, the return type of the divide function is Option<int> - an amplified integer. Note that in this case, the interface of the Divide method is uniform because it is captured entirely in the return type. Pattern matching with the match clause allows the compiler to ensure that both result sub-types are handled explicitly. It follows that, unlike in the C# example, it unnatural to write functional F# code which defers handling of exceptional cases. One drawback of this technique however, is that it can become cumbersome to compose functions which return amplified types. We shall address this composition challenge in the following sections.
For a formal look at the origins of functional approaches to addressing exceptions and beyond take a look at Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell.
Let us return to the original inventory domain. In the original implementation, validation was performed in the function which executed an inventory item command:
module private Assert =
let validName name = if System.String.IsNullOrEmpty(name) then invalidArg "name" "The name must not be null." else name
let validCount count = if count <= 0 then invalidArg "count" "Inventory count must be positive." else count
let inactive item = if item.isActive = true then failwith "The item is already deactivated."
let exec item =
let apply event =
let newItem = apply item event
event
function
| Create(name) -> name |> Assert.validName |> Created |> apply
| Deactivate -> item |> Assert.inactive; Deactivated |> apply
| Rename(name) -> name |> Assert.validName |> Renamed |> apply
| CheckInItems(count) -> count |> Assert.validCount |> ItemsCheckedIn |> apply
| RemoveItems(count) -> count |> Assert.validCount |> ItemsRemoved |> apply
In this example, a failed assertion will raise an exception which will bubble to an outer layer. While it certainly attains the desired result of preventing execution of the behavior, we will refine this implementation with explicit types.
The first order of business is finding an amplified type to encode both successful and erroneous results. While the Option type, used in the integer division example above, captures both cases properly it doesn’t provide any insight into why the result was erroneous. Fortunately, F# provides the Choice type. Much like Option, Choice is a union type, the difference being that it also allows association of an arbitrary value with the second case. We shall use a two-case Choice type to encode success with the first case and an error expressed as a list of string messages with the second case. Thus, the result type of the exec function will be Choice<’TEvent, string list> instead of just ‘TEvent. A basic implementation follows.
let exec state =
function
| Create name ->
match name with
| null | "" -> Choice2Of2 ["The name must not be null or empty."]
| name -> Choice1Of2 (Created(name))
In this example, like the successful result, the erroneous result is returned explicitly. While this implementation avoids many of the pitfalls of exceptions, the syntax could certainly use some work. To address the syntax, we shall put the compositional facilities of F# to work.