Okay, strange question here. I'm using FSharp.Data.SqlClient
to get records from our Database. The records that it infers have several fields which are option types. I need to filter out the records where the ANY of the option types are None and create new records where the fields are known. The following is an example of what I am talking about. To solve this I created a filter function, recordFilter
, which returns the type I want in the case that all of the types of Option<'T>
contain a value and a None
when they do not.
My question is whether it is possible to create a function which just automatically checks all of the Option<'T>
fields in the record for having a value. I'm guessing this would require reflection of some kind to iterate through the fields of the record. I'm guessing this isn't possible but I wanted to throw this out there in case I'm wrong.
If this approach is the idiomatic way, then I would be happy to hear that. I just wanted to make sure that I was not missing out on some more elegant solution. What is possible with F# consistently surprises me.
My motivation is that I am dealing with records with dozens of fields which have a type of Option<'T>
. It is annoying to have to write out a massive match...with
statement as I do in this example. When it is only a few fields is fine, when it is 30+ fields, it is annoying.
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
This can indeed be done with reflection. The namespace FSharp.Reflection
contains some handy helpers for working specifically with F# types, not with .NET in general. The key points to consider are these:
FSharpType.GetRecordFields
returns a list of PropertyInfo
objects for each record field.option
by comparing its type to typedefof<option>
.None
is represented as null
at runtime.FSharpValue.GetUnionFields
and FSharpValue.GetRecordFields
return lists of union or record field values respectively.FSharpValue.MakeRecord
creates a new record given list of its field values.Here is the code:
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
Usage:
> 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;}
NOTE: the above code does not perform any sanity checks (e.g. that 'a
and 'b
are indeed records, or that their fields are indeed namesakes and in the same order, etc.). I leave this as an exercise for the reader :-)
NOTE 2: be careful with performance. Since this is reflection, it is slower and cannot be optimized at compile time.