According to Elm Architecture, all application logic is concentrated in one place. This is a fairly simple and convenient approach, but with the growth of the application, you can see the update
function with a length of 700 lines, Msg
with a hundred designers and Model
that does not fit in the screen.
Such code is quite difficult to study and, often, maintain. I would like to demonstrate a very simple technique that will improve the level of abstractions in your application.
Let's take a simple example.
To start, create a small application with only one text field. Full code can be found here .
type alias Model = { name : String } view : Model -> Html Msg view model = div [] [ input [ placeholder "Name", value model.name, onInput ChangeName ] [] ] type Msg = ChangeName String update : Msg -> Model -> Model update msg model = case msg of ChangeName newName -> { model | name = newName }
As the app grows, we add the last name, "About Me" and the "Save" button. Commit here .
type alias Model = { name : String , surname : String , bio : String } view : Model -> Html Msg view model = div [] [ input [ placeholder "Name", value model.name, onInput ChangeName ] [] , br [] [] , input [ placeholder "Surname", value model.surname, onInput ChangeSurname ] [] , br [] [] , textarea [ placeholder "Bio", onInput ChangeBio, value model.bio ] [] , br [] [] , button [ onClick Save ] [ text "Save" ] ] type Msg = ChangeName String | ChangeSurname String | ChangeBio String | Save update : Msg -> Model -> Model update msg model = case msg of ChangeName newName -> { model | name = newName } ChangeSurname newSurname -> { model | surname = newSurname } ChangeBio newBio -> { model | bio = newBio } Save -> ...
Nothing remarkable, all is well.
But the complexity increases dramatically when we decide to add another component to our page that is not at all related to the existing one - a form for a dog. Commit
type Msg = ChangeName String | ChangeSurname String | ChangeBio String | Save | ChangeDogName String | ChangeBreed String | ChangeDogBio String | SaveDog update : Msg -> Model -> Model update msg model = case msg of ChangeName newName -> { model | name = newName } ChangeSurname newSurname -> { model | surname = newSurname } ChangeBio newBio -> { model | bio = newBio } Save -> ... ChangeDogName newName -> { model | dogName = newName } ChangeBreed newBreed -> { model | breed = newBreed } ChangeDogBio newBio -> { model | dogBio = newBio } SaveDog -> ...
Already at this stage, you can see that Msg
contains two "groups" of messages. My “programming flair” suggests that such things need to be abstracted. What happens when 5 more components appear? And what about the subcomponents? To navigate this code will be almost impossible.
Can we introduce this additional level of abstraction? Of course !
type Msg = HoomanEvent HoomanMsg | DoggoEvent DoggoMsg type HoomanMsg = ChangeHoomanName String | ChangeHoomanSurname String | ChangeHoomanBio String | SaveHooman type DoggoMsg = ChangeDogName String | ChangeDogBreed String | ChangeDogBio String | SaveDog update : Msg -> Model -> Model update msg model = case msg of HoomanEvent hoomanMsg -> updateHooman hoomanMsg model DoggoEvent doggoMsg -> updateDoggo doggoMsg model updateHooman : HoomanMsg -> Model -> Model updateHooman msg model = case msg of ChangeHoomanName newName -> { model | name = newName } -- Code skipped -- updateDoggo : DoggoMsg -> Model -> Model -- Code skipped -- view : Model -> Html Msg view model = div [] [ h3 [] [ text "Hooman" ] , input [ placeholder "Name", value model.name, onInput (HoomanEvent << ChangeHoomanName) ] [] , -- Code skipped -- , button [ onClick (HoomanEvent SaveHooman) ] [ text "Save" ] , h3 [] [ text "Doggo" ] , input [ placeholder "Name", value model.dogName, onInput (DoggoEvent << ChangeDogName) ] [] , -- Code skipped -- ]
When disposing of the Elm type system, we divided our messages into two types: human and canine. Now the threshold of entry into this code will become much easier. As soon as some developer needs to change something in one of the components, he will be able to determine which parts of the code he needs from the type structure. Need to add logic to save dog information? Look at the messages and start searching them.
Imagine your code is a huge reference. How will you look for information of interest to you? On the table of contents (Msg and Model). Will it be easy for you to navigate the table of contents without dividing into sections and subsections? Hardly.
This is an extremely simple technique that can be used anywhere and is fairly easy to embed in existing code. Refactoring an existing application will be completely painless, thanks to static typing and our favorite elm compiler.
By spending only an hour of your time (we spent less than 20 minutes on each project on each application), you can significantly improve the readability of your code and set the standard for how to write it in the future. Good is not the code in which it is easy to correct errors, but one that prohibits errors and sets an example of how the code should be written.
Exactly the same technique can be applied to the Model
, highlighting the necessary information in the types. For example, in our example, the model can be divided into just two types: Hooman
and Doggo
, reducing the number of fields in the model to two.
God save the Elm type system.
PS code repository can be found here if you want to see diffs
Source: https://habr.com/ru/post/455553/
All Articles