[英]F# pattern matching on records with optional fields
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)
除非有其他的东西?
在此示例中,您可以使用:
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 )
我解释的问题是你想让类型系统始终反映这样一个事实,即集合中的那些记录实际上包含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
属性,如其他答案所示。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.