2017-08-09 91 views
2

好的,这里有个奇怪的问题。我正在使用FSharp.Data.SqlClient从我们的数据库中获取记录。它推断的记录有几个选项类型的字段。我需要过滤掉任何选项类型都是None的记录,并在字段已知的情况下创建新记录。以下是我正在谈论的一个例子。为了解决这个问题,我创建了一个过滤器函数recordFilter,它在所有类型的Option<'T>包含一个值时返回我想要的类型,当它们不包含时则返回NoneF#过滤多种选项的记录

我的问题是是否有可能创建一个函数,它只是自动检查记录中的所有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 
+0

是否可以使用int选项列表而不是attr1,attr2 ... atr6? – JosephStevens

+0

@JosephStevens这只是一个玩具的例子。实际上它是int,string,decimal等的混合体。我只是用int来表示这个具体的例子 –

+0

Gotcha,然后是的,你将需要使用反射,因为反射并不是很好,因为它给你编译时间错误带来了一个恶意的运行时错误习惯。 – JosephStevens

回答

4

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

  1. FSharpType.GetRecordFields返回PropertyInfo对象的每个记录的字段列表。
  2. 您可以通过将其类型与typedefof<option>进行比较来判断属性是否为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:小心性能。由于这是反映,因此速度较慢,无法在编译时进行优化。

+0

谢谢你的出色答案! –