4

好吧,奇怪的问题来了。我FSharp.Data.SqlClient用来从我们的数据库中获取记录。它推断的记录有几个选项类型的字段。我需要过滤掉 ANY 选项类型为 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
4

1 回答 1

5

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

  1. FSharpType.GetRecordFields返回PropertyInfo每个记录字段的对象列表。
  2. option您可以通过将其类型与 进行比较来判断属性是否为typedefof<option>
  3. None表示为null在运行时。
  4. FSharpValue.GetUnionFieldsFSharpValue.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

用法:

> 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;}

注意:上面的代码不执行任何完整性检查(例如,'a并且'b确实是记录,或者它们的字段确实是同名的并且以相同的顺序等)。我将此作为练习留给读者:-)

注2:注意性能。由于这是反射,因此速度较慢且无法在编译时进行优化。

于 2017-08-09T17:37:47.490 回答