1
\$\begingroup\$

I've done up a simple app for returning doses of medications for the treatment of a particular medical problem at various stages of treatment. I would like suggestions for how to better organize the code and minimize any needless performance bottlenecks. On an app this simple this is unlikely to be an issue, but I'd like to recognize inefficient patterns I'm using before moving on to more complex projects.

Main.elm:

module Main exposing (..)

import Html
import Element exposing (..)
import Element.Events exposing (onBlur, onClick)
import Element.Attributes exposing (..)
import MyStyle exposing (..)


type alias Menu =
    { label : String
    , group : Group
    , choices : List Choice
    }


type alias Choice =
    { label : String
    , item : Item
    }


type Item
    = Diazepam
    | Lorazepam
    | Midazolam
    | Phenytoin
    | Fosphenytoin
    | Valproate
    | Levetiracetam
    | Phenobarbital
    | MidazolamSedation
    | Propofol
    | Pentobarbital
    | PhenobarbitalSedation
    | Topiramate
    | Zonisamide
    | Lacosamide
    | Ketamine
    | Methylprednisolone
    | IVIG
    | NoneChosen


toString : Item -> String
toString item =
    case item of
        Diazepam ->
            "Diazepam"

        Lorazepam ->
            "Lorazepam"

        Midazolam ->
            "Midazolam"

        Phenytoin ->
            "Phenytoin"

        Fosphenytoin ->
            "Fosphenytoin"

        Valproate ->
            "Valproate"

        Levetiracetam ->
            "Levetiracetam"

        Phenobarbital ->
            "Phenobarbital"

        MidazolamSedation ->
            "Midazolam gtt"

        Propofol ->
            "Propofol"

        Pentobarbital ->
            "Pentobarbital"

        PhenobarbitalSedation ->
            "Phenobarbital gtt"

        Topiramate ->
            "Topiramate"

        Zonisamide ->
            "Zonisamide"

        Lacosamide ->
            "Lacosamide"

        Ketamine ->
            "Ketamine"

        Methylprednisolone ->
            "Methylprednisolone"

        IVIG ->
            "IVIG"

        NoneChosen ->
            "No Medication Chosen"


firstLineMenu : Menu
firstLineMenu =
    { label = "First line agents"
    , group = FirstLine
    , choices =
        [ { label = "Diazepam"
          , item = Diazepam
          }
        , { label = "Lorazepam"
          , item = Lorazepam
          }
        , { label = "Midazolam"
          , item = Midazolam
          }
        ]
    }


secondLineMenu : Menu
secondLineMenu =
    { label = "Second line agents"
    , group = SecondLine
    , choices =
        [ { label = "Fosphenytoin"
          , item = Fosphenytoin
          }
        , { label = "Phenytoin"
          , item = Phenytoin
          }
        , { label = "Levetiracetam"
          , item = Levetiracetam
          }
        , { label = "Valproate"
          , item = Valproate
          }
        , { label = "Phenobarbital"
          , item = Phenobarbital
          }
        ]
    }


thirdLineMenu : Menu
thirdLineMenu =
    { label = "Third line agents"
    , group = ThirdLine
    , choices =
        [ { label = "Midazolam"
          , item = MidazolamSedation
          }
        , { label = "Propofol"
          , item = Propofol
          }
        , { label = "Pentobarbital"
          , item = Pentobarbital
          }
        , { label = "Phenobarbital"
          , item = PhenobarbitalSedation
          }
        ]
    }


fourthLineMenu : Menu
fourthLineMenu =
    { label = "Fourth line agents"
    , group = FourthLine
    , choices =
        [ { label = "Topiramate"
          , item = Topiramate
          }
        , { label = "Zonisamide"
          , item = Zonisamide
          }
        , { label = "Lacosamide"
          , item = Lacosamide
          }
        , { label = "Ketamine"
          , item = Ketamine
          }
        , { label = "Methylprednisolone"
          , item = Methylprednisolone
          }
        , { label = "IVIG"
          , item = IVIG
          }
        ]
    }


type WtUnit
    = Lb
    | Kg
    | NotIndicated


type alias Model =
    { firstLine : Item
    , secondLine : Item
    , thirdLine : Item
    , fourthLine : Item
    , activeMenu : Group
    , lastGroup : Group
    , lastSelection : Item
    , activeMedDose : Maybe String
    }


model : Model
model =
    { firstLine = NoneChosen
    , secondLine = NoneChosen
    , thirdLine = NoneChosen
    , fourthLine = NoneChosen
    , activeMenu = NoGroup
    , lastGroup = NoGroup
    , lastSelection = NoneChosen
    , activeMedDose = Nothing
    }


type Group
    = FirstLine
    | SecondLine
    | ThirdLine
    | FourthLine
    | NoGroup


menuChoice : Model -> Choice -> Element MyStyles variation Msg
menuChoice model choice =
    case model.activeMenu of
        FirstLine ->
            if
                choice.item
                    == model.firstLine
            then
                el Selected [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text (choice.label))
            else
                el Selection [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text choice.label)

        SecondLine ->
            if
                choice.item
                    == model.secondLine
            then
                el Selected [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text (choice.label))
            else
                el Selection [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text choice.label)

        ThirdLine ->
            if
                choice.item
                    == model.thirdLine
            then
                el Selected [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text (choice.label))
            else
                el Selection [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text choice.label)

        FourthLine ->
            if
                choice.item
                    == model.fourthLine
            then
                el Selected [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text (choice.label))
            else
                el Selection [ onClick (SelectChoice choice), padding 6, paddingLeft 12, width (px 178) ] (text choice.label)

        _ ->
            el None [] empty


prepend : List a -> List a -> List a
prepend listA listB =
    List.append listB listA


menuDown : Model -> Menu -> Element MyStyles variation Msg
menuDown model menu =
    wrappedColumn Content
        [ alignLeft, alignTop, padding 10, paddingTop 0, width (px 300), height (px 60) ]
        (if menu.group == model.activeMenu then
            (case model.activeMenu of
                FirstLine ->
                    (List.append
                        [ row ActiveMenuHeader [ verticalCenter, alignLeft, onClick (CloseMenu), padding 8, paddingLeft 16, width (px 290) ] [ text menu.label ] ]
                        [ column ActiveMenuTop
                            [ center, paddingTop 20, width (px 180), height (px 239), alignTop, inlineStyle [ ( "z-index", "999" ) ] ]
                            (List.map
                                (\choice ->
                                    menuChoice model choice
                                )
                                menu.choices
                            )
                        ]
                    )

                SecondLine ->
                    (List.append
                        [ row ActiveMenuHeader [ verticalCenter, alignLeft, onClick (CloseMenu), padding 8, paddingLeft 16, width (px 290) ] [ text menu.label ] ]
                        [ column ActiveMenu
                            [ center, moveUp 60, paddingTop 20, width (px 180), height (px 239), alignTop, inlineStyle [ ( "z-index", "999" ) ] ]
                            (List.map
                                (\choice ->
                                    menuChoice model choice
                                )
                                menu.choices
                            )
                        ]
                    )

                ThirdLine ->
                    (List.append
                        [ row ActiveMenuHeader [ verticalCenter, alignLeft, onClick (CloseMenu), padding 8, paddingLeft 16, width (px 290) ] [ text menu.label ] ]
                        [ column ActiveMenu
                            [ center, moveUp 120, paddingTop 20, width (px 180), height (px 239), alignTop, inlineStyle [ ( "z-index", "999" ) ] ]
                            (List.map
                                (\choice ->
                                    menuChoice model choice
                                )
                                menu.choices
                            )
                        ]
                    )

                FourthLine ->
                    (List.append
                        [ row ActiveMenuHeader [ verticalCenter, alignLeft, onClick (CloseMenu), padding 8, paddingLeft 16, width (px 290) ] [ text menu.label ] ]
                        [ column ActiveMenuBottom
                            [ center, moveUp 180, paddingTop 20, width (px 180), height (px 239), alignTop, inlineStyle [ ( "z-index", "999" ) ] ]
                            (List.map
                                (\choice ->
                                    menuChoice model choice
                                )
                                menu.choices
                            )
                        ]
                    )

                _ ->
                    [ el Header [] empty ]
            )
         else
            [ row Header [ verticalCenter, alignLeft, onClick (OpenMenu menu), padding 8, paddingLeft 16, width fill ] [ text menu.label ] ]
        )


menuUp : Model -> Menu -> Element MyStyles variation Msg
menuUp model menu =
    row None
        [ alignBottom ]
        [ column Content
            []
            (if menu.group == model.activeMenu then
                prepend [ el Header [ onClick (CloseMenu) ] (text menu.label) ]
                    (List.map
                        (\choice ->
                            menuChoice model choice
                        )
                        menu.choices
                    )
             else
                [ el Header [ onClick (OpenMenu menu) ] (text menu.label) ]
            )
        ]



-- UPDATE --


update : Msg -> Model -> Model
update msg model =
    case msg of
        CloseMenu ->
            { model | activeMenu = NoGroup }

        OpenMenu menu ->
            { model | activeMenu = menu.group }

        SelectChoice choice ->
            markSelected model choice

        NoOp ->
            model


type Msg
    = CloseMenu
    | OpenMenu Menu
    | SelectChoice Choice
    | NoOp


updateActiveDose model selection =
    Just (dosing selection.item)


markSelected : Model -> Choice -> Model
markSelected model selection =
    case model.activeMenu of
        FirstLine ->
            { model
                | firstLine = selection.item
                , activeMenu = NoGroup
                , lastSelection = selection.item
                , activeMedDose = updateActiveDose model selection
                , lastGroup = model.activeMenu
            }

        SecondLine ->
            { model
                | secondLine = selection.item
                , activeMenu = NoGroup
                , lastSelection = selection.item
                , activeMedDose = updateActiveDose model selection
                , lastGroup = model.activeMenu
            }

        ThirdLine ->
            { model
                | thirdLine = selection.item
                , activeMenu = NoGroup
                , lastSelection = selection.item
                , activeMedDose = updateActiveDose model selection
                , lastGroup = model.activeMenu
            }

        FourthLine ->
            { model
                | fourthLine = selection.item
                , activeMenu = NoGroup
                , lastSelection = selection.item
                , activeMedDose = updateActiveDose model selection
                , lastGroup = model.activeMenu
            }

        _ ->
            model


dosing : Item -> String
dosing drug =
    case drug of
        Lorazepam ->
            "2-4 mg iv q5 minutes to maximum of 0.1 mg/kg"

        Diazepam ->
            "5 mg iv q10 minutes to maximum of 30 mg OR 20 mg given IR"

        Midazolam ->
            "10 mg im if iv access not immediately available"

        Fosphenytoin ->
            "20 mg/kg PE (phenytoin equivalents) iv administered at 150 mg/min with cardiac and BP monitoring"

        Phenytoin ->
            "20 mg/kg iv administered at 50 mg/min with cardiac and BP monitoring"

        Valproate ->
            "30 mg/kg iv administered over 10 minutes (caution if surgical or hemorrhagic case due to platelet effects)"

        Levetiracetam ->
            "2000 - 4000 mg iv at 500 mg/min"

        Phenobarbital ->
            "20 mg/kg iv administered at 50-100 mg/min"

        MidazolamSedation ->
            "0.1 mg/kg/hr initial dose titrated to sedation to max of 2.9 mg/kg per hour"

        Propofol ->
            "1-2 mg/kg iv load with infusion rate of 33 mcg/kg/min (1.98 mg/kg/hr) titrated to sedation or max of 250 mcg/kg/min (15 mg/kg/hr)"

        Pentobarbital ->
            "Load 15 mg/kg at 50 mg/min and start infusion with rate of 1 mg/kg/hr titrated to suppression or max of 5 mg/kg/hr"

        PhenobarbitalSedation ->
            "Load 15 mg/kg with 50-100mg iv TID maintenance"

        Topiramate ->
            "200-400 mg po or per ng BID titrated daily up to 1600 mg/day (risk of renal calculi)"

        Zonisamide ->
            "100 mg po or per ng daily titrated daily up to 400 mg/day (risk of renal calculi)"

        Lacosamide ->
            "100mg iv BID titrated up to 400mg/day (potential PR prolongation, cardiac arrhythmias)"

        Ketamine ->
            "Load 2 mg/kg bolus then 1 mg/kg/hr titrated to seizure freedom or max of 5 mg/kg/hr"

        Methylprednisolone ->
            "1000mg iv daily for five days"

        IVIG ->
            "0.4 grams/kg daily for 5 days"

        NoneChosen ->
            "None Selected"


view : Model -> Html.Html Msg
view model =
    Element.viewport stylesheet <|
        column Content
            [ width fill, height fill, center ]
            [ column None
                [ paddingTop 20, width fill, center ]
                [ menuDown model firstLineMenu
                , menuDown model secondLineMenu
                , menuDown model thirdLineMenu
                , menuDown model fourthLineMenu
                ]
            , column None
                [ width fill, padding 10, paddingTop 5, center ]
                (case model.lastGroup of
                    FirstLine ->
                        [ paragraph Header
                            [ padding 10, paddingBottom 5, width content, width (px 280) ]
                            [ text "Treating stage 1 of status epilepticus" ]
                        , paragraph Content
                            [ padding 10, paddingTop 5, width (px 280), height content ]
                            [ text "Seizure has lasted 5 minutes or more and/or multiple discrete seizures without clinical recovery between events." ]
                        ]

                    SecondLine ->
                        [ paragraph Header
                            [ padding 10, paddingBottom 5, width content, width (px 280) ]
                            [ text "Treating stage 2 of status epilepticus" ]
                        , paragraph Content
                            [ padding 10, paddingTop 5, width (px 280), height content ]
                            [ text "Seizures continue despite adequate benzodiazepine administration. In practice this stage and stage 1 are typically concurrent." ]
                        ]

                    ThirdLine ->
                        [ paragraph Header
                            [ padding 10, paddingBottom 5, width content, width (px 280) ]
                            [ text "Treating stage 3 of status epilepticus" ]
                        , paragraph Content
                            [ padding 10, paddingTop 5, width (px 280), height content ]
                            [ text "Refractory status epilepticus. Sedation with continuous EEG monitoring. Goal is burst-suppression with interburst intervals of ~10 seconds." ]
                        ]

                    FourthLine ->
                        [ paragraph Header
                            [ padding 10, paddingBottom 5, height content, width (px 280) ]
                            [ text "Treating stage 4 of status epilepticus" ]
                        , paragraph Content
                            [ padding 10, paddingTop 5, width (px 280), height content ]
                            [ text "Super refractory status epilepticus.  Little evidence to guide choice of agent.  Prognosis generally guarded." ]
                        ]

                    _ ->
                        [ empty ]
                )
            , (case model.activeMedDose of
                Nothing ->
                    el None
                        [ hidden ]
                        empty

                Just dose ->
                    paragraph Content
                        [ padding 10, onClick CloseMenu, width (px 280), height content ]
                        [ text
                            ("Dose for " ++ (toString model.lastSelection) ++ " is: " ++ dose)
                        ]
              )
            ]


main =
    Html.beginnerProgram
        { model = model
        , update = update
        , view = view
        }

And the MyStyle.elm being imported:

module MyStyle exposing (..)

import Color
import Style
import Style.Font as Font
import Style.Color as Color
import Style.Border as Border


type MyStyles
    = Header
    | SubHead
    | Content
    | Selection
    | Selected
    | Alerts
    | ActiveMenu
    | ActiveMenuTop
    | ActiveMenuBottom
    | ActiveMenuHeader
    | None


type MyColors
    = Primary
    | DarkPrimary
    | LightPrimary
    | Secondary
    | DarkSecondary
    | LightSecondary
    | Alert
    | Highlight
    | White
    | Black


type MyFonts
    = Headline
    | PrimarySans
    | SecondarySans
    | PrimarySerif
    | SecondarySerif
    | Fallback


fontStack : MyFonts -> Style.Font
fontStack font =
    case font of
        Headline ->
            Font.importUrl { url = "https://fonts.googleapis.com/css?family=Dosis", name = "dosis" }

        PrimarySans ->
            Font.importUrl { url = "https://fonts.googleapis.com/css?family=Lato", name = "lato" }

        SecondarySans ->
            Font.sansSerif

        PrimarySerif ->
            Font.importUrl { url = "https://fonts.googleapis.com/css?family=Esteban", name = "esteban" }

        SecondarySerif ->
            Font.serif

        Fallback ->
            Font.sansSerif


colorPalette : MyColors -> Color.Color
colorPalette mycolor =
    case mycolor of
        DarkPrimary ->
            Color.rgb 34 32 32

        LightPrimary ->
            Color.rgb 142 129 129

        Primary ->
            Color.rgb 53 42 42

        DarkSecondary ->
            Color.rgb 96 125 139

        LightSecondary ->
            Color.rgb 220 232 243

        Secondary ->
            Color.rgb 194 205 214

        Highlight ->
            Color.rgb 218 247 166

        Alert ->
            Color.rgb 232 53 53

        White ->
            Color.rgb 255 255 255

        Black ->
            Color.rgb 0 0 0


stylesheet =
    Style.styleSheet
        [ Style.style Header
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette LightSecondary)
            , Border.rounded 25
            , Border.solid
            , Font.size 36
            , Font.typeface
                [ fontStack Headline
                , fontStack Fallback
                ]
            ]
        , Style.style ActiveMenuHeader
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette Highlight)
            , Border.roundTopLeft 25
            , Border.roundBottomLeft 25
            , Font.size 36
            , Font.typeface
                [ fontStack Headline
                , fontStack Fallback
                ]
            ]
        , Style.style ActiveMenu
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette LightSecondary)
            , Border.rounded 25
            ]
        , Style.style ActiveMenuTop
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette LightSecondary)
            , Border.rounded 25
            , Border.roundTopLeft 0
            ]
        , Style.style ActiveMenuBottom
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette LightSecondary)
            , Border.rounded 25
            , Border.roundBottomLeft 0
            ]
        , Style.style SubHead
            [ Color.text (colorPalette LightSecondary)
            , Font.size 28
            , Font.typeface
            ]
        , Style.style Alerts
            [ Color.background (colorPalette Alert)
            , Color.text (colorPalette DarkSecondary)
            ]
        ]
      [ fontStack PrimarySans
                , fontStack Fallback
                ]
            ]
        , Style.style Content
            [ Color.background (colorPalette LightSecondary)
            , Color.text (colorPalette Primary)
            , Font.size 18
            , Font.typeface
                [ fontStack PrimarySans
                , fontStack Fallback
                ]
            ]
        , Style.style Selection
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette LightSecondary)
            , Font.size 17
            , Style.hover
                [ Color.background (colorPalette Highlight)
                , Color.text (colorPalette Primary)
                ]
            ]
        , Style.style Selected
            [ Color.background (colorPalette Primary)
            , Color.text (colorPalette Highlight)
            , Font.size 17
            , Style.hover
                [ Color.background (colorPalette Highlight)
                , Color.text (colorPalette Primary)
                ]
            ]
        , Style.style Alerts
            [ Color.background (colorPalette Alert)
            , Color.text (colorPalette DarkSecondary)
            ]
        ]
\$\endgroup\$
  • \$\begingroup\$ @Matthew_Stevenson : Hi, how do you build this? When I run your code with elm reactor I get: "The MyStyle module has a bad import: import Color" (I added the mdgriffith/style-elements to elm.json; but Color should be available in elm/core, right?). Maybe you can add your elm.json? \$\endgroup\$ – Gregor Ophey Oct 18 '18 at 18:57
  • 1
    \$\begingroup\$ @Matthew_Stevenson : Regardless of the error, you could use your toString function when creating the choices to make the code more maintainable by not repeating the same strings over and over ... \$\endgroup\$ – Gregor Ophey Oct 18 '18 at 19:01
  • \$\begingroup\$ @GregorOphey : If you are using elm 0.19 the core/Color module has been deprecated and this will not build. It was originally done with elm 0.18. Not why I did it at the time, but toString has also been deprecated in 0.19, so if I were re-visiting this today I'd either implement my own drugToString function or leave it with the repetition. \$\endgroup\$ – Matthew Stevenson Oct 18 '18 at 19:42
  • \$\begingroup\$ Oh, sorry I misunderstood your comment about toString. I thought about using it in the label field of the buttons, for instance, but I didn't like defining a field value based on the value of another field. It also didn't seem any more efficient from a typing standpoint. I could have refactored the choices to a function that maps over a list of drugs and calls the toString on each for the label field. \$\endgroup\$ – Matthew Stevenson Oct 18 '18 at 20:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.