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