Last active
November 5, 2019 03:02
-
-
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#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Arguments Against
There is also the
type Alias
option in something like:type
and thecase
both share the same name.Following discussion in Slack it seems you can use
unType
e.g.type Name = { UnName : string }
rather thanValue
(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