Предотвращение дублирования кода в F # при создании приложения на основе форм для нескольких типов данных - PullRequest
1 голос
/ 27 июня 2019

В настоящее время я делаю приложение на 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 Ответ

2 голосов
/ 28 июня 2019

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

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 соответствующих функций. Что я действительно хотел, так это чтобы они имели тип entry (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 # - ваш друг здесь: когда вы глубоко погружены в рефакторинг, подобный этому, вы не всегда будете помнить, какие функции вы уже изменили, а какие нет. Просто посмотрите на красные волнистые линии, и вы поймете, над какими функциями вам еще нужно работать. Надеемся, что этот пример вдохновит вас на поиск другого распространенного кода, который можно извлечь в его собственную функцию.

...