Type augmentation

The opposite of generalization is specialization, and it is associated with type augmentation in F#. It is worth noting that the official F# 4.0 Language Specification (http://fsharp.org/specs/language-spec/4.0/FSharpSpec-4.0-latest.pdf) does not introduce this terminology using type extension instead. Nevertheless, the type augmentation expression is de-facto ubiquitous and used interchangeably with type extension. Personally, I believe that augmentation is free of the undesired connotation that extension carries as something that's added to an existing matter. Augmentation is a better synonym for the specialization of an existing matter by adding, customizing, or even removing features. So we will stick to it here.

The following figure shows two flavors of type augmentation available in F#. Both use the same syntax but represent different use cases. Intrinsic augmentation customizes your own code, while optional augmentation may customize types outside of your code:

Type augmentation

F# flavors of type augmentation

But why would you need to customize your own code in the first place? This is where certain F#-specific patterns of usage kick in. They can be spotted time and again both inside the F# core libraries and outside in third-party extensions. They explain how a bare type gets created first and then acquires an associated module that carries some helper functions and finally, how the type gets extended by some static methods. We can consider the definition of complex type (https://github.com/fsprojects/powerpack-archive/blob/master/src/FSharp.PowerPack/math/complex.fs) as a manifestation of this pattern:

The preceding definition may be considered as a very neat template of idiomatic intrinsic augmentation.

Let me walk you through some typical use cases of augmenting types.

Augment by removing

At first glance, augment by removing may sound like an oxymoron. However, it is not; just bear with me. Take a look at the following code snippet (Ch10_4.fsx):

type Outcome = 
| Success 
| Failure 
with 
    member x.IsFailure = 
        match x with 
        | Failure -> true 
        | _ -> false 
    member x.IsSuccess = not x.IsFailure 

Here, my intent was to hide the pattern matching behind properties of the discriminated union type Outcome. However, suddenly, this seemingly innocuous piece of code does not compile, as the following screenshot shows:

Augment by removing

F# DU implementation detail leaks out

The F# compiler accompanies the red squiggle line under the IsFailure property name with a surprise message (refer to the preceding screenshot), prompting that the compiler also augments each <Name> use case of discriminated unions with the Is<Name> private property by default, and by defining the identically named property, we made this detail leak out.

Can we effectively remove default compiler-generated augmentation from the Outcome definition? It so happens that we can do that using the .NET attribute especially designated for this purpose: DefaultAugmentation (https://msdn.microsoft.com/visualfsharpdocs/conceptual/core.defaultaugmentationattribute-class-%5bfsharp%5d).

If we just decorate the Outcome type definition with the [<DefaultAugmentation(false)>] attribute, then everything gets back to the intuitively expected behavior, and the property name clash shown above vanishes.

Augment by adding

Now let me do quite the opposite and augment types by adding features. I'm going to use a real (simplified, of course) case from Jet.com technology practices.

Imagine that Jet's e-commerce platform supports the following transaction kinds (Ch10_4.fsx):

type Sale = 
    | DirectSale of decimal 
    | ManualSale of decimal 
 
type Refund = 
    | Refund of decimal 

When we aggregate these transactions for payment or analytics purposes, it is highly desired that you operate upon collections that represent any mix of valid transactions. But we cannot mix different types in a typed collection, can we?

The naive brute-force approach might be exploiting the fact that any .NET type is a subtype of System.Object. So, the following collection might be perfectly OK (Ch10_4.fsx):

let ll: obj list = [box (DirectSale 10.00M); box (Refund -3.99M)] 

However, this approach wipes out one of the major benefits of F#, namely static type safety, which means that unfortunately, having the following collection is also perfectly OK from the standpoint of the F# compiler:

let ll': obj list = [box (Refund -3.99M); box 1; box "Anything"] 

A hardcore OOP developer would continue to lean on inheritance, introducing something like the Transaction superclass as shown here (Ch10_4.fsx):

type Transaction = 
  | Sale of Sale 
  | Refund of Refund 
 
let ll: Transaction list = [Sale (DirectSale 5.00M); Sale (ManualSale 5.00M); Refund (Refund.Refund -1.00M)] 

This is an acceptable approach, but it's not flexible from the standpoint of potential future extensions. Also it is awkward overall.

Any other ideas? Yes, type augmentation comes to the rescue! Well, in a sense.

Let me define a dummy marker interface  ITransaction, as following:

type ITransaction = interface end 

Now, unfortunately, F# does not allow you to add an interface to an already defined type later. But we can still define our transactions, augmenting the standard DU as following (Ch10_4.fsx):

type Sale = 
    | DirectSale of decimal 
    | ManualSale of decimal 
    interface ITransaction 
 
type Refund = 
    | Refund of decimal 
    interface ITransaction 

Furthermore, we can use the idiomatic trick with F# supporting function contravariant arguments:

let mixer (x: ITransaction) = x 

Now we can represent the sought-for collection as following:

let ll: list<_> = [mixer(DirectSale 10.00M); mixer(Refund -3.99M)] 

So far, so good. Now ll is strongly typed ITransaction list, but it can carry any current (and future, if required) transaction kinds. Having the latter mixed together is not a big deal if a disassembly back to concrete transactions is needed, as following (Ch10_4.fsx):

#nowarn "25" 
 
let disassemble (x: ITransaction) = 
    match x with 
    | :? Sale as sale -> (function DirectSale amount -> (sprintf 
"%s%.2f" "Direct sale: " amount, amount) 
    | ManualSale amount -> (sprintf "%s%.2f" "Manual sale: " amount,
amount)) sale 
    | :? Refund as refund -> (function Refund amount -> (sprintf
"%s%.2f" "Refund: " amount, amount)) refund 

(The cryptic turning off the compiler warning "25" in the beginning of the above script addresses the manner the matching by type works. The F# compiler assumes that it may be more types "implementing" ITransaction than are included into preceding match expression. I know that I covered all cases there, so the warning would be just a noise.)

Equipped with this machinery, it is easy to perform, for example, an aggregation of a list of concrete transactions in a single payment (Ch10_4.fsx):

[mixer(DirectSale 4.12M);mixer(Refund -0.10M);mixer(ManualSale 3.62M)] 
|> List.fold (fun (details, total) transaction -> 
    let message, amount = disassemble transaction in 
    (message::details, total + amount)) 
    ([],0.00M) 
|> fun (details,total) -> 
    (sprintf "%s%.2f" "Total: " total) :: details 
|> List.iter (printfn "%s") 

Running the preceding script in FSI will produce the results shown in the following screenshot:

Augment by adding

Augmenting DU with the marker interface

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

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