让我们说有两个联合,其中一个是另一个的严格子集。
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 :(
同样理想的是,如果更改了子集类型,因此不再是子集,那么编译器应该抱怨:
type Subset =
| A of int
| B of string
| D of bool // - no longer a subset of Superset!
我相信这是不可能的,但仍然值得一问(至少要了解为什么这是不可能的)
为什么需要它
我在域中广泛使用这种类型的集合/子集类型来限制实体的不同状态中的有效参数/使无效状态无法表示,并发现该方法非常有益,唯一的缺点是子集之间的繁琐的向上转换。 / p>
答案 0 :(得分:3)
对不起,但这是不可能的。看一下https://fsharpforfunandprofit.com/posts/fsharp-decompiled/#unions,您会发现F#将已区分的联合编译为.NET类,每个联合彼此独立,没有共同的祖先(当然,除了Object
之外)。编译器不会尝试尝试识别不同DU之间的子集或超集。如果它按照您建议的方式工作了,那将是一个重大突破,因为这样做的唯一方法是使子集DU成为基类,而超集类使其派生类具有额外的属性。这将使以下代码更改行为:
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名称猜测超集/子集,那么PhoneNumber
将被视为EmailAddress
的子集,因此将成为其基类。然后,当此函数接收到EmailAddress
对象时,:? PhoneNumber
测试将成功(因为派生类的实例始终可以转换为其基类的类型)。然后,代码将进入匹配表达式的 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
如果您的子集只有两种常见情况,那么这并不是一个巨大的节省。但是,如果您的子集大约有十几个案例,那么能够将这些十几个作为一个单元传递就可以使此设计具有吸引力。