对具有可选字段的记录进行 F# 模式匹配

F# pattern matching on records with optional fields

F# 的 'options' 似乎是一种使用类型系统将已知存在的数据与可能存在或可能不存在的数据分开的好方法,我喜欢 match 的方式表达式强制考虑所有情况:

match str with
| Some s -> functionTakingString(s)
| None -> "abc"         // The compiler would helpfully complain if this line wasn't present

s(相对于 str)是 string 而不是 string option,这非常有用。

但是,在处理具有可选字段的记录时...

type SomeRecord =
    {
        field1 : string
        field2 : string option
    }

...并且这些记录正在被过滤,match 表达式 感觉 是不必要的,因为在 None 情况下没有什么明智的做法,但是这个...

let functionTakingSequenceOfRecords mySeq =
    mySeq
    |> Seq.filter (fun record -> record.field2.IsSome)
    |> Seq.map (fun record -> functionTakingString field2)          // Won't compile

...不会编译,因为虽然没有填充field2的记录已经被过滤掉了,但是field2的类型仍然是string option,而不是string.

我可以定义另一种记录类型,其中 field2 不是可选的,但这种方法看起来很复杂,并且可能不适用于许多可选字段。

我已经定义了一个运算符,如果选项是 None...

,它会引发异常
let inline (|?!) (value : 'a option) (message : string) =
    match value with
    | Some x -> x
    | None -> invalidOp message

...并将之前的代码更改为...

let functionTakingSequenceOfRecords mySeq =
    mySeq
    |> Seq.filter (fun record -> record.field2.IsSome)
    |> Seq.map (fun record -> functionTakingString (record.field2 |?! "Should never happen"))           // Now compiles

...但似乎不太理想。我可以使用 Unchecked.defaultof 而不是引发异常,但我不确定那样更好。关键是 None 案例在过滤后不相关。

有没有更好的方法来处理这个问题?

编辑

非常有趣的答案引起了我对记录模式匹配的注意,这是我不知道的,Value,我见过但误解了(我看到它抛出 NullReferenceException 如果 None)。但我认为我的例子可能很糟糕,因为我的更复杂的现实生活问题涉及使用记录中的多个字段。我怀疑我遇到了类似...

|> Seq.map (fun record -> functionTakingTwoStrings record.field1 record.field2.Value)

除非有别的东西?

在此示例中,您可以使用:

let functionTakingSequenceOfRecords mySeq =
    mySeq
    |> Seq.choose (fun { field2 = v } -> v)
    |> Seq.map functionTakingString

Seq.choose 允许我们根据可选结果过滤项目。在这里,我们对记录进行模式匹配以获得更简洁的代码。

我认为一般的想法是使用组合器、高阶函数来操纵选项值,直到您想将它们转换为其他类型的值(例如,在这种情况下使用 Seq.choose)。不鼓励使用 |?!,因为它是部分运算符(在某些情况下会抛出异常)。您可以争辩说在这种特殊情况下使用它是安全的;但 F# 类型系统无法检测到它并警告您任何情况下的不安全使用。

附带说明一下,我建议您看看 http://fsharpforfunandprofit.com/posts/recipe-part2/ 上的面向铁路的编程系列。该系列向您展示了类型安全和可组合的错误处理方法,您可以在其中保留诊断信息。

更新(根据您的编辑):

你的函数修改后的版本是这样写的:

let functionTakingSequenceOfRecords mySeq =
    mySeq
    |> Seq.choose (fun { field1 = v1; field2 = v2 } -> 
         v2 |> Option.map (functionTakingString v1))

它展示了我提到的一般想法,您可以使用高阶函数 (Option.map) 操纵选项值并在最后一步 (Seq.choose) 转换它们。

既然你找到了 IsSome 属性,你可能也看到了 Value 属性。

let functionTakingSequenceOfRecords mySeq =
    mySeq
    |> Seq.filter (fun record -> record.field2.IsSome)
    |> Seq.map (fun record -> functionTakingString record.field2.Value )

有一个模式匹配的替代方案:

let functionTakingSequenceOfRecords' mySeq =
    mySeq
    |> Seq.choose (function
        | { field2 = Some v } ->  functionTakingString v |> Some
        | _ -> None )

我解释的问题是,您希望类型系统始终反映集合中的那些记录实际上包含字段 2 中的字符串这一事实。

我的意思是,你当然可以使用 choose 来过滤掉你不关心的记录,但你最终还是会得到一个带有可选字符串的记录集合,而且你知道所有这些记录都会是一些字符串。

一种替代方法是像这样创建通用记录:

type SomeRecord<'T> =
{
    field1 : string
    field2 : 'T
}

但是你不能使用记录表达式克隆记录并同时更改记录的通用类型。需要手动创建新记录,如果其他字段不多且结构稳定,问题不大。

另一种选择是将记录包装在具有所需值的元组中,这是一个示例:

let functionTakingSequenceOfRecords mySeq =
    let getField2 record = 
        match record with
        | {field2 = Some value} -> Some (value, record)
        | _ -> None

    mySeq
    |> Seq.choose getField2
    |> Seq.map (fun (f2, {field1 = f1}) -> functionTakingTwoStrings f1 f2)

所以在这里你忽略 field2 的内容,而是使用元组中的第一个值。

除非我误解了你的问题并且你不关心再次进行模式匹配,或者使用警告或#nowarn 指令进行不完全匹配,或者使用选项的 .Value 属性 作为显示在其他答案中。