Предотвращение дублирования кода в F# при создании приложения на основе форм для нескольких типов данных

В настоящее время я делаю приложение на F# с архитектурой fable elmish, в котором типы записей следующие (сокращено, чтобы сэкономить место, но, надеюсь, вы поняли идею).

type NewOriginMerdEntry =
    | AddOriginMerdID of string
    | AddMerdNumber of int
    | AddAverageWeight of float
    | AddPD of int

type NewTreatmentEntry =
    | AddTreatmentID of string

type NewDestMerdEntry =
    | AddDestMerdID of string

 ....etc

Теперь я скомпилировал их в различный тип объединения, такой как этот

type NewEntry =
    | NewOriginMerdEntry of NewOriginMerdEntry
    | NewTreatmentEntry of NewTreatmentEntry
    | NewDestMerdEntry of NewDestMerdEntry
    ...etc

наконец, основной тип сообщения выглядит следующим образом:

type Msg = {
     NewEntry of NewEntry
}

Что достаточно чисто, однако в функции представления мне нужно создать новую функцию для каждого типа, символизирующую представление и конкретные сообщения, которые необходимо отправлять при изменении ввода текста.

Что-то вроде этого:

let originMerdView (dispatch : Msg -> unit) (model : Model) =
    let dispatch' = NewOriginMerdEntry >> NewEntry >> dispatch
    let form = match model.form with
                | OriginMerd o -> o
                | _ -> None

    R.scrollView[
        P.ViewProperties.Style [
            P.FlexStyle.FlexGrow 1.
            P.BackgroundColor "#000000"
        ]
    ][
        //these functions are simply calls to various input text boxes
        inputText "ID" AddOriginMerdID dispatch'
        numinputText "MerdNumber" AddMerdNumber dispatch'
        floatinputText "average Weight" AddAverageWeight dispatch'
        numinputText "PD" AddPD dispatch'
        button "save" form SaveOriginMerd (SaveEntry >> dispatch)
    ]


let inputText label msg dispatch =


    R.textInput[

        P.TextInput.OnChangeText ( msg >> dispatch )
    ]

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

Также каждая новая запись будет отправлена ​​этой функции:

let handleNewEntry (model : Model) (entry : NewEntry) =
    match entry with
    | NewOriginMerdEntry e -> handleNewOriginMerdEntry model e
    ... etc


let handleNewOriginMerdEntry (model : Model) (entry : NewOriginMerdEntry) =
    let form =
        match model.form with
        | OriginMerd o -> match o with
                            | Some f -> f
                            | None -> OriginMerd.New
        | _ -> failwithf "expected origin type got something else handleNewOriginMerd"

    let entry =
        match entry with
        | AddOriginMerdID i -> {form with originMerdID = i}
        | AddMerdNumber n -> {form with merdNumber = n}
        | AddPD p -> {form with pD = p}
        | AddAverageWeight w -> {form with averageWeight = w}

    {model with form = OriginMerd (Some entry)}, Cmd.none

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

1 ответ

Решение

Мне кажется, что эта часть, по крайней мере, на ваш взгляд, будет распространена:

let form = match model.form with
           | OriginMerd o -> o  // With a different match target each time
           | _ -> None

R.scrollView[
    P.ViewProperties.Style [
        P.FlexStyle.FlexGrow 1.
        P.BackgroundColor "#000000"
    ]
]

Я думаю, вы могли бы вытащить это в свою собственную функцию, и просто список полей ввода будет другим. И, что самое важное, передайте части вашей модели этим функциям. Например,

let originMerdForm (dispatch : Msg -> unit) (OriginMerd form) =
    let dispatch' = NewOriginMerdEntry >> NewEntry >> dispatch
    [
        //these functions are simply calls to various input text boxes
        inputText "ID" AddOriginMerdID dispatch'
        numinputText "MerdNumber" AddMerdNumber dispatch'
        floatinputText "average Weight" AddAverageWeight dispatch'
        numinputText "PD" AddPD dispatch'
        button "save" form SaveOriginMerd (SaveEntry >> dispatch)
    ]

let destMerdForm (dispatch : Msg -> unit) (DestMerd form) =
    let dispatch' = NewDestMerdEntry >> NewEntry >> dispatch
    [
        inputText "ID" AddDestMerdID dispatch'
        button "save" form SaveDestMerd (SaveEntry >> dispatch)
    ]

let getFormFields (model : Model) =
    match model.form with
    | OriginMerd _ -> originMerdForm model.form
    | DestMerd _ -> destMerdForm model.form
    // etc.
    | _ -> []

let commonView (dispatch : Msg -> unit) (model : Model) =
    R.scrollView[
        P.ViewProperties.Style [
            P.FlexStyle.FlexGrow 1.
            P.BackgroundColor "#000000"
        ]
    ] (getFormFields model)

Обратите внимание, что, как я написал это, вы получите предупреждение "неполное совпадение" на OriginMerd form а также DestMerd form части соответствующих функций. Я действительно хотел, чтобы у них был тип записи (o в вашей OriginMerd o строка вашего исходного кода), но я не знаю, как вы это назвали. Единственное изменение, которое должно произойти, это то, что вы хотите извлечь button призыв к общему мнению, например,

let commonView (dispatch : Msg -> unit) (model : Model) =
    let formFields, saveMsg = getFormFields model
    R.scrollView[
        P.ViewProperties.Style [
            P.FlexStyle.FlexGrow 1.
            P.BackgroundColor "#000000"
        ]
    ] (formFields @ [button "save" model.form saveMsg (SaveEntry >> dispatch))

а потом твой originMerdForm, destMerdForm вернет кортеж (form fields, msg) где msg было бы SaveOriginMerd, SaveDestMerd, и так далее.

Твой handleNewFooEntry Функции также могут извлечь выгоду из аналогичного изменения входных параметров: вместо передачи всей модели вы можете передать только соответствующий тип записи (и переименовать ваш entry параметр для msgпожалуйста, чтобы не путать себя). Т.е. это будет выглядеть примерно так:

let handleNewEntry (model : Model) (msg : NewEntry) =
    let form' =
        match msg, model.form with
        | NewOriginMerdEntry m, OriginMerd o -> handleNewOriginMerdEntry o m
        | NewOriginMerdEntry m, _ -> failwithf "expected origin type got something else"
        | NewDestMerdEntry m, DestMerd d -> handleNewDestMerdEntry d m
        | NewDestMerdEntry m, _ -> failwithf "expected dest type got something else"
    {model with form = form'}, Cmd.none

let handleNewOriginMerdEntry (formOpt : OriginMerdEntry option) (msg : NewOriginMerdEntry) =
    let form = formOpt |> Option.defaultValue OriginMerd.New
    let result =
        match msg with
        | AddOriginMerdID i -> {form with originMerdID = i}
        | AddMerdNumber n -> {form with merdNumber = n}
        | AddPD p -> {form with pD = p}
        | AddAverageWeight w -> {form with averageWeight = w}
    OriginMerd (Some result)

let handleNewDestMerdEntry (formOpt : DestMerdEntry option) (msg : NewDestMerdEntry) =
    let form = formOpt |> Option.defaultValue DestMerd.New
    let result =
        match msg with
        | AddDestMerdID i -> {form with destMerdID = i}
    DestMerd (Some result)

Каждый раз, когда вы говорите: "Эй, здесь много повторений", обычно есть способ извлечь его из общей функции. Система типа F# - ваш друг здесь: когда вы глубоко погружены в рефакторинг, подобный этому, вы не всегда будете помнить, какие функции вы уже изменили, а какие нет. Просто посмотрите на красные волнистые линии, и вы поймете, над какими функциями вам еще нужно работать. Надеемся, что этот пример вдохновит вас на поиск другого распространенного кода, который можно извлечь в его собственную функцию.

Другие вопросы по тегам