Mat*_*ews 4 f# f#-data fsharp.data.sqlclient
好吧,奇怪的问题在这里。我正在使用FSharp.Data.SqlClient从我们的数据库中获取记录。它推断出的记录有几个字段,它们是选项类型。我需要过滤掉任何选项类型都为 None 的记录,并在已知字段的地方创建新记录。下面是我正在谈论的一个例子。为了解决这个问题,我创建了一个过滤器函数,recordFilter在所有类型都Option<'T>包含值的情况下返回我想要的类型,None当它们不包含时返回我想要的类型。
我的问题是是否有可能创建一个函数来自动检查Option<'T>记录中的所有字段是否有值。我猜这需要某种反射来遍历记录的字段。我猜这是不可能的,但我想把它扔掉,以防万一我错了。
如果这种方法是惯用的方式,那么我会很高兴听到这个。我只是想确保我不会错过一些更优雅的解决方案。F# 的可能性始终让我感到惊讶。
我的动机是我正在处理几十个字段的记录,这些字段的类型为Option<'T>. match...with像我在这个例子中所做的那样,不得不写出大量的语句是很烦人的。只有几个字段的时候还好,30+个字段的时候就烦了。
type OptionRecord = {
Id: int
Attr1: int option
Attr2: int option
Attr3: int option
Attr4: int option
Attr5: int option
Attr6: int option
}
type FilteredRecord = {
Id: int
Attr1: int
Attr2: int
Attr3: int
Attr4: int
Attr5: int
Attr6: int
}
let optionRecords = [for i in 1..5 ->
{
OptionRecord.Id = i
Attr1 = Some i
Attr2 =
match i % 2 = 0 with
| true -> Some i
| false -> None
Attr3 = Some i
Attr4 = Some i
Attr5 = Some i
Attr6 = Some i
}]
let recordFilter (x:OptionRecord) =
match x.Attr1, x.Attr2, x.Attr3, x.Attr4, x.Attr5, x.Attr6 with
| Some attr1, Some attr2, Some attr3, Some attr4, Some attr5, Some attr6 ->
Some {
FilteredRecord.Id = x.Id
Attr1 = attr1
Attr2 = attr2
Attr3 = attr3
Attr4 = attr4
Attr5 = attr5
Attr6 = attr6
}
| _, _, _, _, _, _ -> None
let filteredRecords =
optionRecords
|> List.choose recordFilter
Run Code Online (Sandbox Code Playgroud)
这确实可以通过反射来完成。命名空间FSharp.Reflection包含一些专门用于 F# 类型而不是一般 .NET 的方便的帮助程序。需要考虑的关键点是:
FSharpType.GetRecordFields返回PropertyInfo每个记录字段的对象列表。option通过将属性类型与 进行比较来判断属性是否为typedefof<option>。None表示为null在运行时。FSharpValue.GetUnionFields并分别FSharpValue.GetRecordFields返回联合或记录字段值的列表。FSharpValue.MakeRecord 给定字段值列表创建一个新记录。这是代码:
open FSharp.Reflection
/// Record with Option-typed fields
type RM = { a: int option; b: string option; c: bool option }
/// Record with same fields, but non-optional
type R = { a: int; b: string; c: bool }
/// Determines if the given property is of type option<_>
let isOption (f: System.Reflection.PropertyInfo) =
f.PropertyType.IsGenericType && f.PropertyType.GetGenericTypeDefinition() = typedefof<option<_>>
/// Returns an array of pairs (propertyInfo, value) for every field of the given record.
let fieldsWithValues (r: 'a) =
Array.zip (FSharpType.GetRecordFields typeof<'a>) (FSharpValue.GetRecordFields r)
/// Determines if the given record has any option-type fields whose value is None.
let anyNones (r: 'a) =
fieldsWithValues r |> Seq.exists (fun (f, value) -> isOption f && isNull value)
/// Given two records, 'a and 'b, where 'a is expected to contain some option-typed
/// fields, and 'b is expected to contain their non-option namesakes, creates a new
/// record 'b with all non-None option values copied from 'a.
let copyOptionFields (from: 'a) (to': 'b) : 'b =
let bFields = FSharpValue.GetRecordFields to'
let aFields = Array.zip (FSharpType.GetRecordFields typeof<'a>) (FSharpValue.GetRecordFields from)
for idx, (f, value) in aFields |> Array.indexed do
if isOption f && not (isNull value) then
let _, values = FSharpValue.GetUnionFields( value, f.PropertyType )
bFields.[idx] <- values.[0] // We know that this is a `Some` case, and it has only one value
FSharpValue.MakeRecord( typeof<'b>, bFields ) :?> 'b
Run Code Online (Sandbox Code Playgroud)
用法:
> anyNones {RM.a = Some 42; b = Some "abc"; c = Some true}
val it : bool = false
> anyNones {RM.a = Some 42; b = Some "abc"; c = None}
val it : bool = true
> let emptyR = {R.a = 0; b = ""; c = false}
> copyOptionFields {RM.a = Some 42; b = Some "abc"; c = Some true} emptyR
val it : R = {a = 42; b = "abc"; c = true;}
> copyOptionFields {RM.a = None; b = Some "abc"; c = None} emptyR
val it : R = {a = 0; b = "abc"; c = false;}
Run Code Online (Sandbox Code Playgroud)
注意:上面的代码不执行任何完整性检查(例如,'a和'b确实是记录,或者它们的字段确实是同名且顺序相同等)。我把它留给读者作为练习:-)
注意 2:注意性能。由于这是反射,因此速度较慢且无法在编译时进行优化。