Skip to content

Instantly share code, notes, and snippets.

@pauldorehill
Last active November 5, 2019 03:02
Show Gist options
  • Save pauldorehill/766ec67c8f74bee1383da637a1141d5c to your computer and use it in GitHub Desktop.
Save pauldorehill/766ec67c8f74bee1383da637a1141d5c to your computer and use it in GitHub Desktop.
A riff on all the ways to create simple wrapper types in F#
open System
[<AutoOpen>]
module Shared =
let equalsOn f thisObj (otherObj : obj) =
match otherObj with
| :? 'T as y -> (f thisObj = f y)
| _ -> false
let hashOn f x = hash (f x)
let compareOn f thisObj (otherObj : obj) =
match otherObj with
| :? 'T as otherObj -> compare (f thisObj) (f otherObj)
| _ -> invalidArg "otherObj" "cannot compare values of different types"
type SingleValueRecord =
{ Value : int }
static member create x = { Value = x}
static member map mapper (x : SingleValueRecord) = { Value = mapper x.Value }
[<Struct>]
type SingleValueRecordStruct =
{ Value : int }
static member create x = { Value = x}
static member map mapper (x : SingleValueRecordStruct) = { Value = mapper x.Value }
// Need to impliement Equals / Comparable
type SingleValueClass(value : int) =
member _.Value = value
static member unwrap (x : SingleValueClass) = x.Value
static member map mapper (x : SingleValueClass) = SingleValueClass(mapper x.Value)
interface IComparable with member this.CompareTo x = compareOn SingleValueClass.unwrap this x
override this.Equals x = equalsOn SingleValueClass.unwrap this x
override this.GetHashCode() = hashOn SingleValueClass.unwrap this
// Struct gives you Equals / Comparable out the box
[<Struct>]
type SingleValueStruct(value : int) =
member _.Value = value
static member unwrap (x : SingleValueStruct) = x.Value
static member map mapper (x : SingleValueStruct) = SingleValueStruct(mapper x.Value)
type SingleCaseDU =
| SingleCaseDU of int
member this.Value = match this with SingleCaseDU x -> x
static member map mapper (SingleCaseDU x) = SingleCaseDU (mapper x)
[<Struct>]
type SingleCaseDUStruct =
| SingleCaseDUStruct of int
member this.Value = match this with SingleCaseDUStruct x -> x
static member map mapper (SingleCaseDUStruct x) = SingleCaseDUStruct (mapper x)
type [<Measure>] UOM
module UOM =
let create (x : int) = x * 1<UOM>
let map mapper (x : int<UOM>) = x |> int |> mapper |> create
// Could use class end or interface end - class if you want methods
type PersonId = interface end
/// PHANTOMS
// Phantom is an interesting solution, but you need to give the compiler some hints
// e.g. always include the return type
type PhantomRecord<'T> =
{ Value : int }
static member create x : PhantomRecord<'T> = { Value = x }
static member map mapper (x : PhantomRecord<'T>) : PhantomRecord<'T> = { Value = mapper x.Value }
// Struct gives you Equals / Comparable out the box
[<Struct>]
type PhantomRecordStruct<'T> =
{ Value : int }
static member create x : PhantomRecordStruct<'T> = { Value = x }
static member map mapper (x : PhantomRecordStruct<'T>) : PhantomRecordStruct<'T> = { Value = mapper x.Value }
// Need to impliement Equals / Comparable
type PhantomClass<'T>(value : int) =
member _.Value = value
static member unwrap (x : PhantomClass<'T>) = x.Value
static member map mapper (x : PhantomClass<'T>) : PhantomClass<'T> = PhantomClass(mapper x.Value)
interface IComparable with member this.CompareTo x = compareOn PhantomClass<'T>.unwrap this x
override this.Equals x = equalsOn PhantomClass<'T>.unwrap this x
override this.GetHashCode() = hashOn PhantomClass<'T>.unwrap this
[<Struct>]
type PhantomStruct<'T>(value : int) =
member _.Value = value
static member map mapper (x : PhantomStruct<'T>) : PhantomStruct<'T> = PhantomStruct(mapper x.Value)
// I believe the `Un` prefix means underlying: an alternative to using Value to avoid type/case name conflation
type PhantomDU<'T> =
| UnPhantom of int
member this.Value = match this with UnPhantom x -> x
static member unwrap (UnPhantom x) = x
static member map mapper (x : PhantomDU<'T>) : PhantomDU<'T> = UnPhantom (mapper x.Value)
static member create (x : int) : PhantomDU<'T> = UnPhantom x
[<Struct>]
type PhantomStructDU<'T> =
| UnPhantomStruct of int
member this.Value = match this with UnPhantomStruct x -> x
static member unwrap (UnPhantomStruct x) = x
static member map mapper (x : PhantomStructDU<'T>) : PhantomStructDU<'T> = UnPhantomStruct (mapper x.Value)
static member create (x : int) : PhantomStructDU<'T> = UnPhantomStruct x
let start = 1
let iEnd = 10000
let arrayLen = 10000
let mapper x = x + 5
let testArray = [| start .. arrayLen |]
printfn "Int"
#time
for _ = start to iEnd do
testArray
|> Array.map mapper
|> ignore
#time
printfn "Record"
#time
for _ = start to iEnd do
testArray
|> Array.map SingleValueRecord.create
|> Array.map (SingleValueRecord.map mapper)
|> ignore
#time
printfn "Record - Struct"
#time
for _ = start to iEnd do
testArray
|> Array.map SingleValueRecordStruct.create
|> Array.map (SingleValueRecordStruct.map mapper)
|> ignore
#time
printfn "Class"
#time
for _ = start to iEnd do
testArray
|> Array.map SingleValueClass
|> Array.map (SingleValueClass.map mapper)
|> ignore
#time
printfn "Struct"
#time
for _ = start to iEnd do
testArray
|> Array.map SingleValueStruct
|> Array.map (SingleValueStruct.map mapper)
|> ignore
#time
printfn "Single Case DU"
#time
for _ = start to iEnd do
testArray
|> Array.map SingleCaseDU
|> Array.map (SingleCaseDU.map mapper)
|> ignore
#time
printfn "Single Case DU - Struct"
#time
for _ = start to iEnd do
testArray
|> Array.map SingleCaseDUStruct
|> Array.map (SingleCaseDUStruct.map mapper)
|> ignore
#time
printfn "UOM"
#time
for _ = start to iEnd do
testArray
|> Array.map UOM.create
|> Array.map (UOM.map mapper)
|> ignore
#time
/// Phantoms
printfn "--- PHANTOMS ---"
printfn "Phantom Record"
#time
for _ = start to iEnd do
testArray
|> Array.map PhantomRecord<PersonId>.create
|> Array.map (PhantomRecord<PersonId>.map mapper)
|> ignore
#time
printfn "Phantom Record Struct"
#time
for _ = start to iEnd do
testArray
|> Array.map PhantomRecordStruct<PersonId>.create
|> Array.map (PhantomRecordStruct<PersonId>.map mapper)
|> ignore
#time
printfn "Phantom Class"
#time
for _ = start to iEnd do
testArray
|> Array.map PhantomClass<PersonId>
|> Array.map (PhantomClass<PersonId>.map mapper)
|> ignore
#time
printfn "Phantom Struct"
#time
for _ = start to iEnd do
testArray
|> Array.map PhantomStruct<PersonId>
|> Array.map (PhantomStruct<PersonId>.map mapper)
|> ignore
#time
printfn "Phantom DU"
#time
for _ = start to iEnd do
testArray
|> Array.map PhantomDU<PersonId>.create
|> Array.map (PhantomDU<PersonId>.map mapper)
|> ignore
#time
printfn "Phantom Struct DU"
#time
for _ = start to iEnd do
testArray
|> Array.map PhantomStructDU<PersonId>.create
|> Array.map (PhantomStructDU<PersonId>.map mapper)
|> ignore
#time
// Int
// Real: 00:00:00.159, CPU: 00:00:00.156, GC gen0: 95, gen1: 0, gen2: 0
// Record
// Real: 00:00:02.139, CPU: 00:00:02.140, GC gen0: 1111, gen1: 547, gen2: 0
// Record - Struct
// Real: 00:00:00.226, CPU: 00:00:00.234, GC gen0: 190, gen1: 0, gen2: 0
// Class
// Real: 00:00:01.864, CPU: 00:00:01.859, GC gen0: 1112, gen1: 554, gen2: 0
// Struct
// Real: 00:00:00.209, CPU: 00:00:00.203, GC gen0: 191, gen1: 1, gen2: 0
// Single Case DU
// Real: 00:00:01.989, CPU: 00:00:01.984, GC gen0: 1111, gen1: 555, gen2: 0
// Single Case DU - Struct
// Real: 00:00:00.231, CPU: 00:00:00.234, GC gen0: 190, gen1: 1, gen2: 0
// UOM
// Real: 00:00:00.194, CPU: 00:00:00.187, GC gen0: 190, gen1: 0, gen2: 0
// --- PHANTOMS ---
// Phantom Record
// Real: 00:00:01.975, CPU: 00:00:01.953, GC gen0: 1111, gen1: 554, gen2: 0
// Phantom Record Struct
// Real: 00:00:00.209, CPU: 00:00:00.203, GC gen0: 191, gen1: 1, gen2: 0
// Phantom Class
// Real: 00:00:01.926, CPU: 00:00:01.921, GC gen0: 1112, gen1: 555, gen2: 0
// Phantom Struct
// Real: 00:00:00.217, CPU: 00:00:00.218, GC gen0: 190, gen1: 1, gen2: 0
// Phantom DU
// Real: 00:00:01.978, CPU: 00:00:01.968, GC gen0: 1111, gen1: 555, gen2: 0
// Phantom Struct DU
// Real: 00:00:01.405, CPU: 00:00:01.390, GC gen0: 377, gen1: 0, gen2: 0
@pauldorehill
Copy link
Author

pauldorehill commented Nov 4, 2019

The complete compliment of ways to make a wrapper type for F# e.g. how to avoid passing around primitives. It seems the use of these are somewhat divisive in the F# community with some lovers 💘 and some haters 😠 . I like them due to point number 2 in the arguments for below.

The 'speed' test (yes its crude and likely meaningless... 😄) was just for a very quick look at the cost of the different ways and the results are as expected choosing between class and struct (structs have a lot less garbage collection and very little speed cost is paid in using them); and since UOM are erased there is zero cost.

Arguments For

  1. Clearly defines the domain
  2. Can associate a set of specific functions to each wrapper (micro) type & use these to easily build your bigger types and your whole domain. This is very useful for UI (MVU), validation, and serialization/deserialization. It also makes it very easy to swap out the underlying implementations of these functions.
  3. Stops mixing up of parameters in functions eg. `
let f (p1 : int) (p2 : int) (p3 : int) = p1 * p3 + p2`
  1. Allows hiding of data on the types so an API can easily evolve over time

Arguments Against

  1. Pollutes the code taking up space and making it more verbose
  2. Its slower than primitives
  3. Well named parameters go a long way
let f (clearName : int) (relevantName : int) (itsMe : int) = clearName * relevantName + itsMe

There is also the type Alias option in something like:

type ClearName = int
type RelevantName = int
type ItsMe = int

let f (x : ClearName) (x2 : RelevantName) (x3 : ItsMe) = x * x2 + x3
  1. Conflation of discriminated union names is confusing e.g. the type and the case both share the same name.
  2. The IL code from single case DU's is a mess

Following discussion in Slack it seems you can use unType e.g. type Name = { UnName : string } rather than Value (with the 'un' assumed to mean underlying). This is a good option to avoid name conflation / if you don't like .Value.

Extra discussion on Slack added the following into the mix:

Phantom Types
UMX - UOM for primitive non-numeric - will allow UOM on strings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment