为多种数据类型制作基于表单的应用程序时避免 F# 中的代码重复

Avoiding code duplication in F# when making a form based app for multiple data types

我目前正在使用 F# 制作一个带有寓言 elmish 架构的应用程序,其中记录类型如下(减少以保存 space 但希望您能理解)。

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

所有的具体句柄新入口函数完全一样,只是记录明显不同。这个功能很好,但代码重用再次非常痛苦。有没有更优雅的方法以更少的代码重复实现相同的结果?

在我看来,至少您的这一部分观点将被分享:

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 formDestMerd form 部分收到 "incomplete match case" 警告。我真正想要的是让它们具有 entry 的类型(原始代码 OriginMerd o 行中的 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))

然后你的 originMerdFormdestMerdForm 会 return 一个 (form fields, msg) 的元组,其中 msg 会是 SaveOriginMerdSaveDestMerd,等等。

您的 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)

任何时候你说,"Hey, there's a lot of repetition here",通常有一种方法可以将它提取到一个通用函数中。 F# 类型系统是您的好帮手:当您 hip-deep 进行这样的重构时,您不会总是记得已经更改了哪些功能以及还没有更改哪些功能。不过,只需寻找红色波浪线,您就会知道哪些功能还需要处理。希望这个示例会启发您发现其他可以提取到其自身功能的通用代码。