F#过滤包含多种选项类型的记录

时间:2017-08-09 16:51:00

标签: f# f#-data fsharp.data.sqlclient

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

我的问题是,是否可以创建一个仅自动检查记录中的所有match...with字段以获取值的函数。我猜这需要某种反射来迭代记录的字段。我猜这是不可能的,但我想把它扔出去,万一我错了。

如果这种方法是惯用的方式,那么我很乐意听到这一点。我只是想确保我没有错过一些更优雅的解决方案。 F#的可能性让我感到惊讶。

我的动机是我正在处理包含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 类型的数十个字段的记录。像我在这个例子中那样写出一个巨大的//Convert bitmap to byte array Bitmap bitmap = mResultsBitmap; ByteArrayOutputStream bos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos); byte[] bitmapdata = bos.toByteArray(); //write the bytes in file FileOutputStream fos = null; try { fos = new FileOutputStream(f); } catch (FileNotFoundException e) { e.printStackTrace(); } try { fos.write(bitmapdata); } catch (IOException e) { e.printStackTrace(); } try { fos.flush(); } catch (IOException e) { e.printStackTrace(); } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } final File pFIle2 = f; //TODO: clarifai stuff //TODO: clarifai stuff Log.e("this:"," this is running 0"); Thread thread = new Thread(new Runnable() { @Override public void run() { Log.e("this:", " this is running 1"); client = new ClarifaiBuilder("mykeyhere1234}").buildSync(); Log.e("this:", " this is running 2"); Thread th = new Thread(new Runnable() { @Override public void run() { Log.e("this:", " this is running 3"); Log.e("this", client.getDefaultModels().generalModel().predict() .withInputs( ClarifaiInput.forImage(ClarifaiImage.of(pFIle2)) ) .executeSync().rawBody()); Log.e("this:", " this is running 4"); } }); } }); 语句是很烦人的。当它只有几个字段是好的,当它是30多个字段时,它很烦人。

{{1}}

1 个答案:

答案 0 :(得分:4)

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

  1. FSharpType.GetRecordFields返回每个记录字段的PropertyInfo个对象列表。
  2. 您可以通过将其类型与option进行比较来判断该属性是否为typedefof<option>
  3. None在运行时表示为null
  4. FSharpValue.GetUnionFieldsFSharpValue.GetRecordFields分别返回联合或记录字段值的列表。
  5. FSharpValue.MakeRecord根据字段值列表创建新记录。
  6. 以下是代码:

    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 :小心表现。由于这是反射,因此速度较慢,无法在编译时进行优化。