Reputation: 526
I use Guids and strings to serve as keys in my data structure. In C#, I have spent many (accumulated) hours wondering why no event came through with the id I was looking for when the id was an OrderId and I was matching it with a ContractId. What I want to do is to prevent this entire class of error.
Imagine I have a contract with the following underlying data types:
type Contract = { Schedule : Guid; TickTable : Guid; Price : float; Quantity : float }
Now I have two problems:
let contract =
{ Schedule = Guid.Empty; TickTable = Guid.Empty; Price = 0.; Quantity = 0. }
contract.Schedule = contract.TickTable;; // true - ugh
contract.Price = contract.Quantity;; // true - ugh
I can fix one problem like this:
[<Measure>] type dollars
[<Measure>] type volume
type Contract =
{ Schedule : Guid; TickTable : Guid;
Price : float<dollars>; Quantity : float<volume> }
Now we have:
let contract =
{ Schedule = Guid.Empty; TickTable = Guid.Empty;
Price = 0.<dollars>; Quantity = 0.<volume> }
contract.Schedule = contract.TickTable;; // true - ugh
contract.Price = contract.Quantity;; // type mismatch - yay
Is there a way I can decorate the Guids so I get a type mismatch? I only really want to impact compile-time - ideally the compiled code would be the same, as in units of measure.
I know I can do the following, but it seems ugly and I would expect it causes run-time impact:
[<Measure>] type dollars
[<Measure>] type volume
type ScheduleId = ScheduleKey of Guid
type TickTableId = TickTableKey of Guid
type Contract =
{ Schedule : ScheduleId; TickTable : TickTableId;
Price : float<dollars>; Quantity : float<volume> }
let contract =
{ Schedule = ScheduleKey Guid.Empty; TickTable = TickTableKey Guid.Empty;
Price = 0.<dollars>; Quantity = 0.<volume> }
contract.Schedule = contract.TickTable;; // type error - yay
contract.Price = contract.Quantity;; // type mismatch - yay
Upvotes: 2
Views: 139
Reputation: 6223
You can wrap any type to have units, even generically, by writing a type with a [<Measure>]
type argument. Also, as latkin hinted in the comments, using a struct (which is allocated in place, not as a new object) would save extra allocation and indirection.
Generic unit-of-measure aware wrapper:
type [<Struct>] UnitAware<'T, [<Measure>] 'u> =
val Raw : 'T
new (raw) = { Raw = raw }
let withUnit<[<Measure>] 'u> a = UnitAware<_, 'u>(a)
This way, arbitrary types can be given a unit-of-measure aware value type wrapper, simply wrapping via withUnit<myUnit>
and unwrapping with .Raw
:
let a = 146L |> withUnit<dollars>
let b = 146L |> withUnit<volume>
a = b // Type mismatch.
Due to structural comparison, two struct wrappers with the same units and with equal contents will also be equal. As with other unit-of-measure usage, the additional type safety is lost at run-time: box a = box b
is true, just like box 1.<dollars> = box 1.<volumes>
.
Upvotes: 3