Lamarth
Lamarth

Reputation: 526

Creating non-comparable versions of a type in F#

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

Answers (1)

Vandroiy
Vandroiy

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

Related Questions