2017-08-09 89 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

謝謝你的出色答案! –