带有可选字段的记录上的F#模式匹配

时间:2015-01-22 13:42:26

标签: f# pattern-matching field record optional

F#' s'选项'似乎是一种使用类型系统将已知存在的数据与可能存在或不存在的数据分开的好方法,我喜欢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,我已经看到但被误解(我看到它抛出一个{ {1}}如果NullReferenceException)。但我认为我的例子可能很差,因为我更复杂,现实生活中的问题涉及从记录中使用多个字段。我怀疑我会遇到像...这样的事情。

None

除非还有别的东西?

3 个答案:

答案 0 :(得分:3)

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

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)转换它们。

答案 1 :(得分: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 )

答案 2 :(得分: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属性,如另一个所示答案。