【问题标题】:F# pattern matching on records with optional fields带有可选字段的记录上的 F# 模式匹配
【发布时间】:2015-03-21 07:57:48
【问题描述】:

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这个case过滤后不相关。

有没有更好的处理方法?

编辑

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

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

除非还有别的?

【问题讨论】:

    标签: f# pattern-matching field record optional


    【解决方案1】:

    我解释的问题是,您希望类型系统始终反映集合中的那些记录实际上包含 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 属性,如其他答案所示。

    【讨论】:

    • 谢谢 - 通用记录和元组方法都是有趣的替代方案。
    【解决方案2】:

    在本例中,您可以使用:

    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) 的一般概念。

    【讨论】:

    • 感谢您提供非常有趣的答案。根据我上面问题中的编辑,我不确定是否可以使用记录模式匹配,但也感谢 Rail-Oriented Programming 链接。
    • @Giles:我根据您的编辑修改了我的答案。我认为我的总体想法仍然有效。您应该努力采用类型安全的方式来转换值。
    • 非常感谢您的更新。我可能需要一点时间才能完全消化,但我想我明白了要点!公平地说,潜在的缺点是,对于包含许多字段的记录,有必要按照 v1 和 v2 绑定所有字段?
    • 是的。在这种情况下,Maybe 计算表达式会很方便使用。
    【解决方案3】:

    既然您找到了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 为了使匹配更加通用,您可以根据需要引入更多record patterns,或者使用"as" pattern 将整个模式绑定到一个变量(例如| { field2 = Some v } as record -&gt; ...)。跨度>
    • 感谢另一个指针 - 我将阅读“as”模式。我在 F# 上的第一周,我意识到还有很多比我迄今为止学到的更多的知识!
    猜你喜欢
    • 2013-10-19
    • 2015-01-25
    • 1970-01-01
    • 2019-01-10
    • 2020-07-20
    • 2020-09-25
    • 1970-01-01
    • 2020-10-21
    • 1970-01-01
    相关资源
    最近更新 更多