具有多种选项类型的 F# 过滤记录

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)

Fyo*_*kin 5

这确实可以通过反射来完成。命名空间FSharp.Reflection包含一些专门用于 F# 类型而不是一般 .NET 的方便的帮助程序。需要考虑的关键点是:

  1. FSharpType.GetRecordFields返回PropertyInfo每个记录字段的对象列表。
  2. 您可以option通过将属性类型与 进行比较来判断属性是否为typedefof<option>
  3. None表示为null在运行时。
  4. FSharpValue.GetUnionFields并分别FSharpValue.GetRecordFields返回联合或记录字段值的列表。
  5. 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:注意性能。由于这是反射,因此速度较慢且无法在编译时进行优化。