Фильтрация записей F# с множеством типов параметров

Ладно, вот странный вопрос. Я использую FSharp.Data.SqlClient для получения записей из нашей базы данных. Записи, которые он выводит, имеют несколько полей, которые являются типами опций. Мне нужно отфильтровать записи, в которых ЛЮБОЙ из типов опций равен 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

person Matthew Crews    schedule 09.08.2017    source источник
comment
Можно ли использовать список вариантов int вместо attr1, attr2... atr6?   -  person JosephStevens    schedule 09.08.2017
comment
@JosephStevens Это просто игрушечный пример. На самом деле это смесь int, string, decimal и т. д. Я просто использовал int для этого конкретного примера.   -  person Matthew Crews    schedule 09.08.2017
comment
Попался, тогда да, вам нужно будет использовать отражение, просто осторожность, поскольку отражение не так хорошо дает вам ошибки времени компиляции, вместо этого у него есть неприятная привычка к ошибкам времени выполнения.   -  person JosephStevens    schedule 09.08.2017


Ответы (1)


Это действительно можно сделать с помощью рефлексии. Пространство имен FSharp.Reflection содержит несколько удобных помощников для работы именно с типами F#, а не с .NET в целом. Ключевые моменты, которые следует учитывать, следующие:

  1. FSharpType.GetRecordFields возвращает список PropertyInfo объектов для каждого поля записи.
  2. Вы можете определить, является ли свойство option, сравнив его тип с typedefof<option>.
  3. None представлен как null во время выполнения.
  4. FSharpValue.GetUnionFields и FSharpValue.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: будьте осторожны с производительностью. Поскольку это отражение, оно медленнее и не может быть оптимизировано во время компиляции.

person Fyodor Soikin    schedule 09.08.2017