Совместное использование функций в разных типах - PullRequest
4 голосов
/ 08 апреля 2019

У нас есть процесс etl, записанный, если f #, который берет данные, отсортированные в реляционной базе данных, и преобразует их в схему типа «звезда», готовую для сторонней платформы.Поскольку мы денормализуем данные, у нас есть (почти) дубликаты объектов, типов и свойств, разбросанных по нашей системе.До сих пор я был доволен этим, потому что объекты достаточно разные, чтобы гарантировать разные функции, или мы смогли сгруппировать общие / общие свойства в подзапись.

Однако теперь мы добавляем объекты, которые должны выбирать и выбирать различные части системы и не попадают в существующую общую группу.После экспериментов с несколькими различными стилями я попал в использование интерфейсов, но что-то не так с их использованием.Кто-нибудь сталкивался с этой проблемой и придумал другой подход?

module rec MyModels =
    type AccountType1 =
        { Id : int
          Error : string option 
          Name : string option }
        // PROBLEM: this get very bulky as more properties are shared
        interface Props.Error<AccountType1> with member x.Optic = (fun _ -> x.Error), (fun v -> { x with Error = v })
        interface Props.AccountId<AccountType1> with member x.Optic = (fun _ -> x.Id), (fun v -> { x with Id = v })
        interface Props.AccountName<AccountType1> with member x.Optic = (fun _ -> x.Name), (fun v -> { x with Name = v })

    type AccountType2 =
        { Id : int
          Error : string option 
          AccountId : int
          AccountName : string option
          OtherValue : string }
        interface Props.Error<AccountType2> with member x.Optic = (fun _ -> x.Error), (fun v -> { x with Error = v })
        interface Props.AccountId<AccountType2> with member x.Optic = (fun _ -> x.AccountId), (fun v -> { x with AccountId = v })
        interface Props.AccountName<AccountType2> with member x.Optic = (fun _ -> x.AccountName), (fun v -> { x with AccountName = v })
        interface Props.OtherValue<AccountType2> with member x.Optic = (fun _ -> x.OtherValue), (fun v -> { x with OtherValue = v })

    module Props =
        type OpticProp<'a,'b> = (unit -> 'a) * ('a -> 'b)    

        // Common properties my models can share
        // (I know they should start with an I)

        type Error<'a> = abstract member Optic : OpticProp<string option, 'a>
        let Error (h : Error<_>) = h.Optic

        type AccountId<'a> = abstract member Optic : OpticProp<int, 'a>
        let AccountId (h : AccountId<_>) = h.Optic

        type AccountName<'a> = abstract member Optic : OpticProp<string option, 'a>
        let AccountName (h : AccountName<_>) = h.Optic

        type OtherValue<'a> = abstract member Optic : OpticProp<string, 'a>
        let OtherValue (h : OtherValue<_>) = h.Optic

[<RequireQualifiedAccess>]
module Optics =
    // Based on Aether
    module Operators =
        let inline (^.) o optic = (optic o |> fst) ()
        let inline (^=) value optic = fun o ->  (optic o |> snd) value

    let inline get optic o =
        let get, _ = optic o
        get ()

    let inline set optic v (o : 'a) : 'a = 
        let _, set = optic o
        set v

open MyModels
open Optics.Operators

// Common functions that change the models

let error msg item =
    item
    |> (Some msg)^=Props.Error
    |> Error

let accountName item = 
    match item^.Props.AccountId with
    | 1 -> 
        item
        |> (Some "Account 1")^=Props.AccountName
        |> Ok
    | 2 -> 
        item
        |> (Some "Account 2")^=Props.AccountName
        |> Ok
    | _ ->
        item
        |> error "Can't find account"

let correctAccount item =
    match item^.Props.AccountName with
    | Some "Account 1" -> Ok item
    | _ ->
        item
        |> error "This is not Account 1"

let otherValue lookup item =
    let value = lookup ()

    item
    |> value^=Props.OtherValue
    |> Ok

// Build the transform pipeline

let inline (>=>) a b =
    fun value ->
    match a value with
    | Ok result -> b result
    | Error error -> Error error


let account1TransformPipeline lookups = // Lookups can be passed around is needed
    accountName
    >=> correctAccount

let account2TransformPipeline lookups =
    accountName
    >=> correctAccount
    >=> otherValue lookups

// Try out the pipelines

let account1 = 
    ({ Id = 1; Error = None; Name = None } : AccountType1)
    |> account1TransformPipeline ()

let account2 = 
    ({ Id = 1; Error = None; AccountId = 1; AccountName = None; OtherValue = "foo" } : AccountType2)
    |> account2TransformPipeline (fun () -> "bar")

Другое, что я пробовал:

  • Эфирная оптика - если я что-то не упустил, этопросто для редактирования подтипов сложного объекта, а не для общих свойств
  • Утиная печать - мне это очень понравилось, но проблема в том, что вы должны встроить слишком много функций

1 Ответ

21 голосов
/ 09 апреля 2019

Я не совсем уверен, как сделать ваше решение проще - я думаю, что очень причудливое использование типов в вашем подходе делает код довольно сложным. Могут быть и другие способы упростить это, сохраняя при этом какое-то печатание. Точно так же я думаю, что есть случаи, когда логика, которую вам нужно реализовать, достаточно динамична, и тогда, возможно, стоит использовать более динамичные методы, даже в F #.

В качестве примера, вот пример того, как сделать это с помощью библиотеки фреймов данных Deedle . Это позволяет вам представлять данные в виде фреймов данных (с именами столбцов в виде строк).

Записать две необходимые операции очистки для фрейма данных относительно легко - библиотека оптимизирована для операций на основе столбцов, поэтому структура кода немного отличается от вашей (мы вычисляем новый столбец, а затем заменяем его для всех строк во фрейме данных):

let correctAccount idCol nameCol df = 
  let newNames = df |> Frame.getCol idCol |> Series.map (fun _ id ->
    match id with
      | 1 -> "Account 1" 
      | 2 -> "Account 2" 
      | _ -> failwith "Cannot find account")
  df |> Frame.replaceCol nameCol newNames

let otherValue newValue df = 
  let newOther = df |> Frame.getCol "OtherValue" |> Series.mapAll (fun _ _ -> Some newValue)
  df |> Frame.replaceCol "OtherValue" newOther

Затем ваш конвейер может принимать записи, преобразовывать их во фреймы данных и выполнять всю обработку:

[ { Id = 1; Error = None; Name = None } ]
|> Frame.ofRecords
|> correctAccount "Id" "Name"

[ { Id = 1; Error = None; AccountId = 1; AccountName = None; OtherValue = "foo" } ]
|> Frame.ofRecords
|> correctAccount "Id" "AccountName"
|> otherValue "bar"

Это менее безопасно для типов, чем ваш подход, но я считаю, что люди действительно могут прочитать код и получить хорошее представление о том, что он делает, что может стоить компромисса.

...