F#中的强类型ID?

时间:2019-05-21 09:40:44

标签: f#

我的应用程序中有两种实体:客户和产品。它们每个都由UUID在数据库级别标识。

在我的F#代码中,可以用System.Guid表示。

为了便于阅读,我添加了一些类似的类型:

open System

type CustomerId = Guid

type ProductId = Guid

但是,这并不妨碍我将ProductId用作CustomerId,反之亦然。

我想出了一个包装器主意来防止这种情况:

open System

[<Struct>]
type ProductId = 
  {
    Product : Guid
  }

[<Struct>]
type CustomerId = 
  {
    Customer : Guid
  }

这使初始化更加冗长,并且可能不那么直观:

let productId = { Product = Guid.NewGuid () }

但是它增加了类型安全性:

// let customerId : CustomerId = productId // Type error

我想知道还有什么其他方法。

2 个答案:

答案 0 :(得分:5)

您可以使用single-case union类型:

open System

[<Struct>]
type ProductId = ProductId of Guid

[<Struct>]
type CustomerId = CustomerId of Guid

let productId = ProductId (Guid.NewGuid())

通常我们直接在类型中添加一些方便的辅助方法/属性:

[<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"

答案 1 :(得分:4)

另一种较不常见的方法,但值得一提的是使用所谓的幻像类型。这个想法是,您将拥有一个通用包装ID<'T>,然后对'T使用不同的类型来表示不同类型的ID。这些类型从未真正实例化,这就是为什么它们被称为 phantom 类型。

[<Struct>]
type ID<'T> = ID of System.Guid

type CustomerID = interface end
type ProductID = interface end

现在,您可以创建ID<CustomerID>ID<ProductID>值来表示两种ID:

let newCustomerID () : ID<CustomerID> = ID(System.Guid.NewGuid())
let newProductID () : ID<ProductID> = ID(System.Guid.NewGuid())

有趣的是,您可以编写可轻松使用任何ID的函数:

let printID (ID g) = printfn "%s" (g.ToString())

例如,我现在可以创建一个客户ID,一个产品ID并同时打印两者,但是由于它们的类型不匹配,我无法对这些ID进行相等性测试:

let ci = newCustomerID ()
let pi = newProductID ()
printID ci
printID pi
ci = pi // Type mismatch. Expecting a 'ID<CustomerID>' but given a 'ID<ProductID>'    

这是一个巧妙的技巧,但是比为每个ID使用新类型要复杂得多。特别是,您可能会在各个地方需要更多的类型注释来使这项工作有效,并且类型错误可能不太清楚,尤其是当涉及通用代码时。但是,值得一提的是它。