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

6

F#的“选项”似乎是一种很好的利用类型系统将已知存在的数据与可能存在或不存在的数据分开的方式,我喜欢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,我看到过但误解了它(如果None,它会抛出一个NullReferenceException)。但我认为我的例子可能不太好,因为我更复杂、真实的问题涉及使用记录中的多个字段。我猜我只能像这样做...

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

除非还有其他问题?
3个回答

4
在这个例子中,您可以使用:
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


非常感谢您提供如此有趣的答案。根据我在上面问题中的编辑,我不确定是否可以使用记录模式匹配,但也感谢您提供的铁路导向编程链接。 - Giles
@Giles:根据您的编辑,我修改了我的答案。我认为我的基本想法仍然有效。您应该努力寻找类型安全的转换值的方法。 - pad
非常感谢您的更新。我可能需要一些时间来完全消化,但我认为我已经理解了要点!可以这样说,潜在的缺点是对于具有许多字段的记录,需要像v1和v2一样绑定所有字段吗? - Giles
1
是的。在这种情况下,Maybe计算表达式可能会很方便使用。 - pad

2

既然您已经找到了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 )

谢谢您建议使用.Value - 我看到过它,但是误解了。我喜欢记录模式匹配,但根据我问题中的编辑,我不确定能否使用它。除非... :o) - Giles
1
为了使匹配更加灵活,您可以根据需要引入更多的“记录模式”,或使用“as模式”将整个模式绑定到变量上(例如 | { field2 = Some v } as record -> ...)。@Giles - kaefer
谢谢你提供的另一个指针 - 我会研究一下 "as" 模式。我在 F# 上的第一周,意识到还有很多东西需要了解,远不止我目前掌握的! - Giles

2

我的理解是,您希望在任何时候都能够使用类型系统来反映集合中的记录实际上包含了field2中的字符串。

我的意思是,当然您可以使用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属性。

谢谢 - 通用记录和使用元组的方法都是有趣的替代方案。 - Giles

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接