Handling dependency loops

I’ve had trouble managing dependencies between modules and I’m looking for advice on what to do in a situation that I often come across. To make it easier to discuss I’m going to use a (simplified) example from my game.

In my game I have a Menu module and a Model module (which contains the top level Model). The Menu has a MenuModel for storing state that’s only relevant while in the user is in the menu. The problem here is that Model.Model uses Menu.MenuModel and Menu.view uses both Model.Msg and Model.Model. So I end up with a dependency loop. The solutions I can think of are to either:

  1. Move Menu.MenuModel into Model
  2. “Component-ize” Menu so that it has it’s own Msg type, update function, and a Config extension record that has all the fields that Menu.view needs from Model.Model

I don’t like the first solution because now any function in my app can modify MenuModel in potentially invalid ways. When the MenuModel was in Menu I could make it opaque and only a small number of functions would have access to its internals*.

I don’t like the second solution either because a lot of extra wiring has to be done to make Menu work with the rest of the app. Sometimes that’s worthwhile if the component is used in multiple places in my game, but the Menu is only used once.

Are there better solutions I have overlooked?

*I can still make MenuModel opaque in Model and then add all the functions that can modify it to the Model as well, but if I use this solution every time I run into a dependency loop then I end up with a Model.elm file that’s ususably large.

I’m not sure if it will help you, but this post describes some high-level strategies:

For your specific case, I see two strategies:

  • Why does Menu.view use Model.Model? Maybe pass just what is needed, not the whole model, and avoid exposing Model.Model. Then how many Model.Msg does Menu.view uses? If there are just a few of them, you can pass them as arguments (maybe into a record) to the Menu.view function instead of exposing Model.Msg (so Menu.view would return Html msg instead of Html Model.Msg) .
  • You could also have some exposed Menu.Msg and use them in your update function without Menu having its own update function. This is not an “all or nothing” choice.

I personally never expose Main.Model or Main.Msg, I may however put some types used by them in dedicated modules.

1 Like

Can you be more specific? Is the Menu a page (so that if the menu is used, the game holds) or is it more of a “collapsable menu”/reusable view, where you just need to store something like “item selected” or “collapsed”? If its like a page, then I would give it its own Model/View/Update. If its just a reusable view, then I would pass a config type with the nessary msgs to the view function.

Maybe if you could post the definition of MenuModel, that might give some more insight.

Menu is a simplification in order to make the problem more explainable. It’s really responsible for all the pages in the main menu. Here’s the model for it

-- This type isn't as relevant but I'm including it just to show why MainMenu_ has an underscore in it
type Game
    = MainMenu MainMenu_
    | Stage Stage_
    | LevelEditor
    | ReplayViewer ReplayViewer.Model

type alias MainMenu_ =
    { backgroundGameplay : Maybe BackgroundGameplay
    , menuPage : MainMenuPage
    , transitionStart : Maybe ( Quantity Float RealTime, MainMenuPage )
    , showTrailer : Bool
    , showReplayParseError : Bool
    , mainMenuIntro : MainMenuIntro
    }

It’s definitely not a reusable component but it does have a Model/view/update (to make things more confusing, it’s not an update, it’s a step. It’s called on each animation frame to handle background gameplay and manage keyboard navigation among other things).

Why does Menu.view use Model.Model ? Maybe pass just what is needed, not the whole model, and avoid exposing Model.Model . Then how many Model.Msg does Menu.view uses? If there are just a few of them, you can pass them as arguments (maybe into a record) to the Menu.view function instead of exposing Model.Msg (so Menu.view would return Html msg instead of Html Model.Msg )

I do use this approach (looks like the article refers to it as dependency inversion). Here’s an example of what that looks like in Menu.elm*.

type alias MsgConfig msg =
    { noOp : msg
    , pressedStartButton : msg
    , pressedEditorButton : msg
    , pressedSettingsButton : msg
    , keyPressedStageSelectBackButton : msg
    , keyPressedStageSelectStartButton : msg
    , keyPressedStageSelectTile : { stageId : StageId } -> msg
    , selectedStageSelecTileWithArrowKeys : { stageId : StageId } -> msg
    , pressedSettingsBackButton : msg
    , pressedColorSettingsBackButton : msg
    }


type alias Config a =
    { a
        | windowSize : Vector2i
        , time : Quantity Float RealTime
        , stepDelta : Quantity Float RealTime
        , mouseDown : Bool
        , previousMouseDown : Bool
        , mousePosition : Point2d
        , previousMousePosition : Point2d
        , spriteSheet : SpriteSheet
        , sounds : Sound.Model
        , antialias : Bool
        , highscoreState : HighscoreTable.Model
        , colorPalette : ColorPalette
        , keys : List Keyboard.Key
        , previousKeys : List Keyboard.Key
        , stageSelectId : StageId
    }

The problem here is that these tend to get large. Here, a majority of these config fields aren’t used directly by Menu and instead are passed onto submodules. I still think this approach is better than using Html.map though.

Another problem is that wiring up update can get messy. Update might end up doing a lot of stuff. It will probably change MenuModel, but it might also return a list of msgs (defined using MsgConfig), or return a Cmd msg. These need to get wired up by the function calling update, and then also any results from Cmd msg need to find there way back to update. In my chosen Menu example fortunately a lot of this doesn’t happen but I have a level editor which is a perfect storm of all of these possible things needing to be wired up.

In the article it suggested splitting modules into an UpdateMenu and a ViewMenu module. I haven’t tried that, maybe it could help. His approach is certainly more structured than my ad hoc approach.

*Actually the module is called MenuState but I wanted to keep the original example simple. Also here’s the source code if you’re interested in having a look

Why? What’s wrong with Html.map?

Avoiding Html.map where Html.map is the best approach could lead to needless complexity.

Personally, I view this as an anti-pattern in Elm. Splitting the triad (Model/Update/View) is a recipe for troubles.

The proper way to do it (in my view) is to split the things that make each part of the triad into modules. So, rather than having a complex Model, maybe some of that complexity can be extracted into a Business Object. This would be some kind of custom type that does something complex. It would have an interface and it would allow the extraction of the code from the component. Smart custom types can make both the initialization of the Model and the update less complex than they would otherwise be. By far, extracting custom types is the best thing you can do. These custom types would get their own deep modules.

Another line of extraction are the views. You can have all kind of “widgets” from simple to complex living in their own view modules, oblivious of the code around them. I usually have a Ui.elm file in the root of the project that captures all small views and a bunch of Ui.WidgetX for complex widgets. Complex enough widgets should get their own module (sometimes with their own triad). This means using Html.map. There is nothing wrong with using components it’s just that people started splitting things into components that had no business being more than a simple function and this lead to the current warnings about Html.map.

Why? What’s wrong with Html.map?

I’ve find it be more verbose and the code isn’t as nicely grouped. Consider this example

module Menu exposing (MsgConfig, view)

type alias MsgConfig msg = 
    { menuMsg1 : msg
    , menuMsg2 : msg
    , menuMsg3 : msg
    }

view msgConfig =
    ...

----- Later in our Main module -----
type Msg 
    = Msg1
    | Msg2
    | Msg3

view = 
    Menu.view 
        { msg1 = Msg1
        , msg2 = Msg2
        , msg3 = Msg3 
        }

in contrast with

module Menu exposing (Msg(..), view)

type Msg 
    = MenuMsg1
    | MenuMsg2
    | MenuMsg3

view =
    ...

----- Later in our Main module -----

type Msg
    = Msg1
    | Msg2
    | Msg3

view =
    Menu.view
        |> Html.map
            (\menuMsg ->
                case menuMsg of
                    Menu.MenuMsg1 -> 
                        Msg1

                    Menu.MenuMsg2 ->
                        Msg2

                    Menu.MenuMsg3 -> 
                        Msg3

            )

The Html.map version takes up more lines. Additionally, in more complicated situations, our MsgConfig can be converted to an extension record so that a parent module can pass along its own MsgConfig without needing to reconstruct it with only the necessary msgs.

Edit: I realized a pretty reasonable counter-argument would be, “Why not add MenuMsg to Main.Msg? Then you can just write Menu.view |> Html.map MenuMsg”. I’ve found this doesn’t work because often, those messages in Menu will need to make some changes to the top level model. Which means Main.update needs to handle them and you’ll end up with a big case statement like before.

This is a Child-Parent communication issue. The way I usually handle this is have the Child.update return a triple of type (Child.Model, Cmd Child.Msg , Child.OutMsg) where type OutMsg = Pass | SomeMessage | SomeOtherMessage.

In the parent I pattern match agains the OutMsg in order to act upon it.

Alternatively, if the Child is not using any side-effects, I sometime have the view type be Html (ChildState, OutMsg) where I update the state directly in the event handler and I send it together with OutMsg to the Parent. The Parent can pattern match easier on what the child produced.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.