F# 是否可以将 "upcast" 区分的联合值转换为 "superset" 联合?
F# is it possible to "upcast" discriminated union value to a "superset" union?
假设有两个联合,其中一个是另一个的严格子集。
type Superset =
| A of int
| B of string
| C of decimal
type Subset =
| A of int
| B of string
是否可以自动将子集值向上转换为超集值而不求助于显式模式匹配?像这样:
let x : Subset = A 1
let y : Superset = x // this won't compile :(
如果 Subset 类型被更改因此它不再是一个子集那么编译器应该抱怨也是理想的:
type Subset =
| A of int
| B of string
| D of bool // - no longer a subset of Superset!
我认为这是不可能的,但仍然值得一问(至少要明白为什么不可能)
为什么我需要它
我在我的域中广泛使用这种 set/subset 类型来限制实体不同状态中的有效参数/使无效状态不可表示并且发现该方法非常有益,唯一的缺点是非常繁琐的向上转换子集之间。
抱歉,没有
抱歉,这是不可能的。看看 https://fsharpforfunandprofit.com/posts/fsharp-decompiled/#unions — 您会看到 F# 将可区分的联合编译为 .NET classes,每个都彼此分离,没有共同的祖先(当然 Object
除外).编译器不会尝试识别不同 DU 之间的子集或超集。如果它确实按照您建议的方式工作,那将是一个重大变化,因为这样做的唯一方法是使子集 DU 成为基础 class,而超集 class 是其派生的 class 加上额外的 属性。这将使以下代码更改行为:
type PhoneNumber =
| Valid of string
| Invalid
type EmailAddress =
| Valid of string
| ValidButOutdated of string
| Invalid
let identifyContactInfo (info : obj) =
// This came from external code we don't control, but it should be contact info
match (unbox obj) with
| :? PhoneNumber as phone -> // Do something
| :? EmailAddress as email -> // Do something
是的,这是糟糕的代码,应该以不同的方式编写,但它说明了这一点。在当前的编译器行为下,如果 identifyContactInfo
传递给 EmailAddress
对象,:? PhoneNumber
测试将失败,因此它将进入匹配的第二个分支,并将该对象(正确地)视为一个电子邮件地址。如果编译器根据您在此处建议的 DU 名称猜测 supersets/subsets,那么 PhoneNumber
将被视为 EmailAddress
的子集,因此将成为其基础 class .然后当这个函数接收到一个 EmailAddress
对象时,:? PhoneNumber
测试就会成功(因为派生 class 的实例总是可以转换为它的基础类型 class ).然后代码将进入匹配表达式的 first 分支,然后您的代码可能会尝试向电子邮件地址发送短信。
但是等等...
通过将子集拉出到它们自己的 DU 类别中,您可能可以实现您想要做的事情:
type AorB =
| A of int
| B of string
type ABC =
| AorB of AorB
| C of decimal
type ABD =
| AorB of AorB
| D of bool
那么 ABC
的匹配表达式可能如下所示:
match foo with
| AorB (A num) -> printfn "%d" num
| AorB (B s) -> printfn "%s" s
| C num -> printfn "%M" num
如果你需要在 ABC
和 ABD
之间传递数据:
let (bar : ABD option) =
match foo with
| AorB data -> Some (AorB data)
| C _ -> None
如果您的子集只有两种常见情况,那节省的钱并不算多。但是,如果您的子集有十几个案例,那么能够将这十几个案例作为一个单元进行传递会使该设计具有吸引力。
假设有两个联合,其中一个是另一个的严格子集。
type Superset =
| A of int
| B of string
| C of decimal
type Subset =
| A of int
| B of string
是否可以自动将子集值向上转换为超集值而不求助于显式模式匹配?像这样:
let x : Subset = A 1
let y : Superset = x // this won't compile :(
如果 Subset 类型被更改因此它不再是一个子集那么编译器应该抱怨也是理想的:
type Subset =
| A of int
| B of string
| D of bool // - no longer a subset of Superset!
我认为这是不可能的,但仍然值得一问(至少要明白为什么不可能)
为什么我需要它
我在我的域中广泛使用这种 set/subset 类型来限制实体不同状态中的有效参数/使无效状态不可表示并且发现该方法非常有益,唯一的缺点是非常繁琐的向上转换子集之间。
抱歉,没有
抱歉,这是不可能的。看看 https://fsharpforfunandprofit.com/posts/fsharp-decompiled/#unions — 您会看到 F# 将可区分的联合编译为 .NET classes,每个都彼此分离,没有共同的祖先(当然 Object
除外).编译器不会尝试识别不同 DU 之间的子集或超集。如果它确实按照您建议的方式工作,那将是一个重大变化,因为这样做的唯一方法是使子集 DU 成为基础 class,而超集 class 是其派生的 class 加上额外的 属性。这将使以下代码更改行为:
type PhoneNumber =
| Valid of string
| Invalid
type EmailAddress =
| Valid of string
| ValidButOutdated of string
| Invalid
let identifyContactInfo (info : obj) =
// This came from external code we don't control, but it should be contact info
match (unbox obj) with
| :? PhoneNumber as phone -> // Do something
| :? EmailAddress as email -> // Do something
是的,这是糟糕的代码,应该以不同的方式编写,但它说明了这一点。在当前的编译器行为下,如果 identifyContactInfo
传递给 EmailAddress
对象,:? PhoneNumber
测试将失败,因此它将进入匹配的第二个分支,并将该对象(正确地)视为一个电子邮件地址。如果编译器根据您在此处建议的 DU 名称猜测 supersets/subsets,那么 PhoneNumber
将被视为 EmailAddress
的子集,因此将成为其基础 class .然后当这个函数接收到一个 EmailAddress
对象时,:? PhoneNumber
测试就会成功(因为派生 class 的实例总是可以转换为它的基础类型 class ).然后代码将进入匹配表达式的 first 分支,然后您的代码可能会尝试向电子邮件地址发送短信。
但是等等...
通过将子集拉出到它们自己的 DU 类别中,您可能可以实现您想要做的事情:
type AorB =
| A of int
| B of string
type ABC =
| AorB of AorB
| C of decimal
type ABD =
| AorB of AorB
| D of bool
那么 ABC
的匹配表达式可能如下所示:
match foo with
| AorB (A num) -> printfn "%d" num
| AorB (B s) -> printfn "%s" s
| C num -> printfn "%M" num
如果你需要在 ABC
和 ABD
之间传递数据:
let (bar : ABD option) =
match foo with
| AorB data -> Some (AorB data)
| C _ -> None
如果您的子集只有两种常见情况,那节省的钱并不算多。但是,如果您的子集有十几个案例,那么能够将这十几个案例作为一个单元进行传递会使该设计具有吸引力。