为什么我不能通过受歧视联合的成员列表来简化此迭代?

Why can't I simplify this iteration through a list of members of a discriminated union?

人们经常想要迭代(使用 map、iter 或 fold)一组异构对象(不同类型)。解决这个问题的一种方法是创建一个可区分的联合,它允许创建一个列表,其中包含适当转换为 DU 案例的对象。以下代码在一个简单示例中执行此操作:

type MYDU = | X1 of int
            | X2 of float
            | X3 of string

let bar (y: MYDU) =
    match y with
    | X1 x -> printfn "%A" x  
    | X2 x -> printfn "%A" x
    | X3 x -> printfn "%A" x

[X1(1); X2(2.0); X3("3"); X1(4)]
|> List.map bar |> ignore

此代码运行良好并打印

1
2.0
"3"
4

太棒了!但我想知道是否可以避免重复调用 printfn。我尝试了以下但它没有编译:

let baz (y: MYDU) =
    match y with
    | X1 x | X2 x | X3 x -> printfn "%A" x // red squiggly line under X1 x

编译器发出此消息:

This expression was expected to have type 'int' but here has type 'float'

我怀疑避免重复是可行的,但我一定犯了一个基本错误。有什么建议吗?

你没有犯错,这不是 F# 的类型系统所允许的。

匹配大小写箭头左侧可以有多个模式,但它们必须绑定同一组值(包括类型)。在这里,x 每个模式都有不同的类型,这足以让编译器报错。

有一些方法可以减轻这种痛苦(你可以在 DU 上有一个成员 return 一个装箱值,或者你可以有一个活动模式来在匹配案例中进行装箱),但它们是高度情景化的。将模式拆分为单独的案例并为每个案例重复右侧始终是真空中更好的解决方案。

您可以做的是将您的参数转换为用于打印的通用类型,然后打印出该值。而且您仍然可以获得模式匹配和可区分联合的优势:)

下面是这种方法的一个例子

type MYDU = 
  | X1 of int
  | X2 of float
  | X3 of string

let bar y =
    let myStr = 
      match y with
      | X1 x -> string x  
      | X2 x -> string x
      | X3 x -> x
    printfn "%s" myStr

bar (X1 5)

可以通过 "boxing" 将值设为 obj 类型来在某种程度上避免这种重复。这是 .NET 的 System.Object: "the ultimate base class of all classes in the .NET Framework" 的别名。这意味着任何类型的任何值也是 obj.

但是,当您装箱一个对象时,您将丢失静态类型。您正在颠覆 F# 类型系统并增加出错的机会。这就是为什么通常应该避免这样做的原因,除非您有充分的理由这样做。

函数 printfn "%A" 可以采用任何类型,因此它的类型签名实际上是 obj -> unit。如果你想做的只是 运行 这个函数的值,那么使用装箱可能被认为是合理的。您可以定义此活动模式,它使用 box 函数:

let (|Box|) x = box x

然后使用这样的模式:

let printMyDu myDu =
    match myDu with
    | X1 (Box x)
    | X2 (Box x)
    | X3 (Box x) -> printfn "%A" x

同样,您应该尽可能避免这样做,因为在许多情况下您会失去类型安全性。例如,如果您只是为了稍后检查该值的类型而将一个值装箱,那么您可能对 F# 采用了错误的方法。在此示例中,我们将值 x 装箱,然后立即使用并丢弃它,因此我们不会降低整体类型安全性。