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:
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:
complex.fs
source line 13 (https://github.com/fsprojects/powerpack-archive/blob/master/src/FSharp.PowerPack/math/complex.fs#L13), the bare type Complex
is defined as having custom equality
and comparison
properties and being the value typecomplex.fs
source line 39 (https://github.com/fsprojects/powerpack-archive/blob/master/src/FSharp.PowerPack/math/complex.fs#L39), the module Complex
is defined as carrying the entire slew of helper functions covering, in particular, all math operations in complex numberscomplex.fs
source line 99 (https://github.com/fsprojects/powerpack-archive/blob/master/src/FSharp.PowerPack/math/complex.fs#L99), type Complex
is supplied by static operators for complex math numbers expressed via previously defined helper functionsThe 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.
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:
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.
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: