Types 不同返回类型的模式匹配

Types 不同返回类型的模式匹配,types,f#,pattern-matching,Types,F#,Pattern Matching,我正在学习F#,并使用类型系统进行领域建模 在我非常简单的例子中,假设我们想要管理一家酒店的客户。客户可以处于各种状态: 新客户 联系人信息已定义 客户接受了GPDR 客户已登记入住 所有这些状态都表示为不同的类型。我们还定义,只要客户尚未登记,但已提供联系信息和/或已接受GPDR,客户就处于“待定”状态: type CustomerId = CustomerId of Guid type ContactInformation = ContactInformation of string typ

我正在学习F#,并使用类型系统进行领域建模

在我非常简单的例子中,假设我们想要管理一家酒店的客户。客户可以处于各种状态:

  • 新客户
  • 联系人信息已定义
  • 客户接受了GPDR
  • 客户已登记入住
  • 所有这些状态都表示为不同的类型。我们还定义,只要客户尚未登记,但已提供联系信息和/或已接受GPDR,客户就处于“待定”状态:

    type CustomerId = CustomerId of Guid
    type ContactInformation = ContactInformation of string
    type AcceptDate = AcceptDate of DateTime
    type CheckInDate = CheckInDate of DateTime
    
    type NewCustomer =
        private
            { Id: CustomerId }
    
    type ContactOnlyCustomer =
        private
            { Id: CustomerId
              Contact: ContactInformation }
    
    type AcceptedGdprCustomer =
        private
            { Id: CustomerId
              Contact: ContactInformation
              AcceptDate: AcceptDate }
    
    type PendingCustomer =
        private
            | ContactOnly of ContactOnlyCustomer
            | AcceptedGdpr of AcceptedGdprCustomer  
    
    type CheckedInCustomer =
        private
            { Id: CustomerId
              Contact: ContactInformation
              AcceptDate: AcceptDate
              CheckInDate: CheckInDate }    
    
    type Customer =
        private
            | New of NewCustomer
            | Pending of PendingCustomer
            | CheckedIn of CheckedInCustomer
    
    现在,我想使用以下功能更新客户的联系信息(无论客户当前处于哪个“状态”):

    这样,我就不需要嵌套模式匹配:

    let updateDetails (customer: Customer) contact =
        match customer with
        | New c -> ContactOnly { Id = c.Id; Contact = contact }
        | ContactOnly c -> ContactOnly { c with Contact = contact }
        | AcceptedGdpr c -> AcceptedGdpr { c with Contact = contact }
        | CheckedIn c -> CheckedIn { c with Contact = contact }
    
    这是可行的,但这似乎不是正确的方法,因为当我还想将
    PendingCustomer
    类型用于其他函数时,这会导致类型定义的重复


    作为一名F#初学者,我觉得我错过了一件简单的小事。

    我认为没有理由拥有如此复杂的模型。这并不意味着没有,但就你在问题中所解释的而言,这似乎满足了你的所有约束条件,同时也非常简单:

    type Customer =
      { Id: CustomerId
      , Contact: ContactInformation option
      , AcceptedGDPR: DateTime option
      , CheckedIn: DateTime option
      }
    
    let updateDetails (customer: Customer) contact =
      { customer with Contact = contact }
    
    我认为您可以简化(例如提取共享状态)并使您的案例更加明确,这将使解决此问题更加容易

    type CustomerId = CustomerId of Guid
    type ContactInformation = ContactInformation of string
    type AcceptDate = AcceptDate of DateTime
    type CheckInDate = CheckInDate of DateTime
    
    type CheckedInCustomer =
        private
            { Contact: ContactInformation
              AcceptDate: AcceptDate
              CheckInDate: CheckInDate }    
    
    type CustomerState =
        private
            | New
            | ContactOnly of ContactInformation
            | AcceptedGdpr of AcceptDate
            | ContactAndGdpr of ContactInformation * AcceptDate
            | CheckedIn of CheckedInCustomer
    
    type Customer =
        private
            { Id: CustomerId
              State: CustomerState }
    
    let updateContact (customer: Customer) contact =
        match customer.State with
        | New -> { customer with State = ContactOnly contact }
        | ContactOnly _ -> { customer with State = ContactOnly contact }
        | AcceptedGdpr acceptDate -> { customer with State = ContactAndGdpr(contact, acceptDate) }
        | ContactAndGdpr (_,acceptDate) -> { customer with State = ContactAndGdpr(contact, acceptDate) }
        | CheckedIn checkedIn -> { customer with State = CheckedIn { checkedIn with Contact = contact } }
    

    您可能还想查看一些库,例如使处理原语类型验证更容易。

    我赞同尝试使用类型来避免非法状态的想法,特别是在涉及到GDPR同意/合同协议等关键事项时

    在评论中讨论了一点后,是否应
    updateContact
    更新
    客户的联系信息

    let updateContact (customer: Customer) (contact : ContactInformation) : Customer =
        match customer with
        | New c -> ContactOnly { Id = c.Id; Contact = contact } |> Pending
        | Pending pending ->
            match pending with
            | ContactOnly c -> ContactOnly { c with Contact = contact } |> Pending
            | AcceptedGdpr c -> AcceptedGdpr { c with Contact = contact } |> Pending
        | CheckedIn c -> CheckedIn { c with Contact = contact }
    

    在原始代码
    updateContact
    中,返回联系人信息,但不返回更新后的客户,这会导致查找适合所有分支机构的表达式类型时出现问题。在这里,所有分支机构都会产生一个
    客户
    来避免这个问题。

    感谢您的回复。“复杂”模型背后的原因是用类型对域逻辑建模,即用类型防止无效状态。例如,只要客户没有提供联系信息并接受GDPR,他们就不能办理入住手续。(我可以定义一个
    checkIn
    函数,它只接受
    accepteddprcustomer
    )您可以通过让转换函数检查条件并返回一个选项,轻松防止在无效状态下签入。正如你所发现的那样,在运行时维护不变量对于静态类型系统通常不是一个很好的用途。我不知道你的意思是什么,@glennsl,不知道你是否错过了关于F类型系统的一些东西。这就是我在想的:@BentTranberg我理解他们在尝试做什么,并且认为在某些情况下这是一个好工具,但这是一个权衡,问题中没有任何东西可以证明复杂性,并表明它是这种情况下的正确工具。您不需要静态类型来强制执行业务规则。这就是代码的用途。只要您封装它,使非法状态只可能在内部出现,这就不是问题。这似乎更像是
    AbstractSingletonProxyFactoryBean
    的功能等价物——机械地应用模式只是因为你可以,而不考虑实际需要。我看不出有任何理由冒犯任何人。只是OP明确表示,其目的是使用类型系统进行域建模,所以这不是一个好答案。在第一个示例中,
    updateContact
    是否也可以返回一个
    Customer
    ,然后在所有分支中使用相同的表达式类型。@Justanothermetaprogrammer是的,这就是我想要实现的。但我想我缺少了一个关于如何做到这一点的技术细节
    ContactOnly
    AcceptedDPR
    只是
    Customer
    的“子类型”(
    PendingCustomer
    ),但对于类型系统,它们是不同的,因此存在编译错误。我不理解在这种情况下重复类型定义是什么意思。如果你指的是在各州重复使用字段,我不会将其视为重复。这不是问题,只要你保持它如此简单,你看不到任何好处,试图做什么。不要过早地进行重构——等到看到有收益时再进行重构。然后你可能也会更清楚地看到需要做什么。至于嵌套类型,这似乎是在状态图中对特定路由进行建模的一种非常糟糕的方法。不要这样做。你可能会发现透镜的想法很有趣,透镜对于更新不可变结构中的嵌套值很有用:
    type CustomerId = CustomerId of Guid
    type ContactInformation = ContactInformation of string
    type AcceptDate = AcceptDate of DateTime
    type CheckInDate = CheckInDate of DateTime
    
    type CheckedInCustomer =
        private
            { Contact: ContactInformation
              AcceptDate: AcceptDate
              CheckInDate: CheckInDate }    
    
    type CustomerState =
        private
            | New
            | ContactOnly of ContactInformation
            | AcceptedGdpr of AcceptDate
            | ContactAndGdpr of ContactInformation * AcceptDate
            | CheckedIn of CheckedInCustomer
    
    type Customer =
        private
            { Id: CustomerId
              State: CustomerState }
    
    let updateContact (customer: Customer) contact =
        match customer.State with
        | New -> { customer with State = ContactOnly contact }
        | ContactOnly _ -> { customer with State = ContactOnly contact }
        | AcceptedGdpr acceptDate -> { customer with State = ContactAndGdpr(contact, acceptDate) }
        | ContactAndGdpr (_,acceptDate) -> { customer with State = ContactAndGdpr(contact, acceptDate) }
        | CheckedIn checkedIn -> { customer with State = CheckedIn { checkedIn with Contact = contact } }
    
    let updateContact (customer: Customer) (contact : ContactInformation) : Customer =
        match customer with
        | New c -> ContactOnly { Id = c.Id; Contact = contact } |> Pending
        | Pending pending ->
            match pending with
            | ContactOnly c -> ContactOnly { c with Contact = contact } |> Pending
            | AcceptedGdpr c -> AcceptedGdpr { c with Contact = contact } |> Pending
        | CheckedIn c -> CheckedIn { c with Contact = contact }