Reputation: 37131
I have two kinds of entity in my application: customers and products. They are each identified at a database level by a UUID.
In my F# code, this can be represented by System.Guid
.
For readability, I added some types like this:
open System
type CustomerId = Guid
type ProductId = Guid
However, this does not prevent me from using a ProductId
as a CustomerId
and vice-versa.
I came up with a wrapper idea to prevent this:
open System
[<Struct>]
type ProductId =
{
Product : Guid
}
[<Struct>]
type CustomerId =
{
Customer : Guid
}
This makes initialization a little more verbose, and perhaps less intuitive:
let productId = { Product = Guid.NewGuid () }
But it adds type-safety:
// let customerId : CustomerId = productId // Type error
I was wondering what other approaches there are.
Upvotes: 4
Views: 1034
Reputation: 243096
Another approach, which is less common, but worth mentioning is to use so-called phantom types. The idea is that you will have a generic wrapper ID<'T>
and then use different types for 'T
to represent different types of IDs. Those types are never actually instantiated, which is why they're called phantom types.
[<Struct>]
type ID<'T> = ID of System.Guid
type CustomerID = interface end
type ProductID = interface end
Now you can create ID<CustomerID>
and ID<ProductID>
values to represent two kinds of IDs:
let newCustomerID () : ID<CustomerID> = ID(System.Guid.NewGuid())
let newProductID () : ID<ProductID> = ID(System.Guid.NewGuid())
The nice thing about this is that you can write functions that work with any ID easily:
let printID (ID g) = printfn "%s" (g.ToString())
For example, I can now create one customer ID, one product ID and print both, but I cannot do equality test on those IDs, because they're types do not match:
let ci = newCustomerID ()
let pi = newProductID ()
printID ci
printID pi
ci = pi // Type mismatch. Expecting a 'ID<CustomerID>' but given a 'ID<ProductID>'
This is a neat trick, but it is a bit more complicated than just using new type for each ID. In particular, you will likely need more type annotations in various places to make this work and the type errors might be less clear, especially when there is generic code involved. However, it's worth mentioning this as an alternative.
Upvotes: 8
Reputation: 3784
You can use single-case union types:
open System
[<Struct>]
type ProductId = ProductId of Guid
[<Struct>]
type CustomerId = CustomerId of Guid
let productId = ProductId (Guid.NewGuid())
Normally we add some convenient helper methods/properties directly to the types:
[<Struct>]
type ProductId = private ProductId of Guid with
static member Create () = ProductId (Guid.NewGuid())
member this.Value = let (ProductId i) = this in i
[<Struct>]
type CustomerId = private CustomerId of Guid with
static member Create () = CustomerId (Guid.NewGuid())
member this.Value = let (CustomerId i) = this in i
let productId = ProductId.Create ()
productId.Value |> printfn "%A"
Upvotes: 9