Search code examples
functional-programmingpolymorphismelmunificationunion-types

How to communicate with a polymorphic child component in Elm?


My main program has an update function of

update : Msg -> Model -> ( Model, Cmd Msg )

To communicate with sub-components we can add another variant and wrap our messages in a new message

type alias Model =
    { ...
    , child : Child.Model
    }

type Msg
    = ...
    | ChildMsg Child.Msg

update msg model =
    case msg of
        ...

        ChildMsg childMsg ->
          let
              ( childModel, cmd ) =
                  Child.update childMsg model.child

              updatedModel =
                  { model | child = childModel }

              childCmd =
                Cmd.map ChildMsg cmd
          in
               ( updatedModel, childCmd )

However this seem challenging if the type of my sub-component's update function does not match the parent. Consider a child with a polymorphic update function:

-- PolymorphicChild.elm

update : Msg a -> Model -> ( Model, Cmd (Msg a) )

When running a command from this module, I must wrap it

PolymorphicChild.someCommand : Cmd (Msg Foo)

PolymorphicChild.someCommand
  |> Cmd.map PolymorphicChild

However, this produces a Msg (PolymorphicChild.Msg Foo), not the Msg PolymorphicChild.Msg my App is expecting.

The right side of (|>) is causing a type mismatch.

(|>) is expecting the right side to be a:

    Cmd (PolyMorphicChild.Msg Foo) -> a

But the right side is:

    Cmd Polymorphic.Msg -> Cmd Msg

I tried adding a polymorphic parameter to App.Msg

-- App.elm

type Msg a =
   = ..
   | PolymorphicChildMsg (PolymorphicChild.Msg a) 

But it basically blows up my entire program. Every function involving App.Msg needs to somehow be changed to work with the new child component.

How can I unify the two types and get the two components working together?


Solution

  • I think the problem is that you're leaking too much information in your publicly exposed Msg type. Your use of the type parameter of Msg a seems limited to a known set of types, either an Author, Category, Post, or Tag. From skimming your code, it looks like it will never be anything but one of those four, so the fact that you are abstracting things in this manner should be kept inside of this module rather than exposing it and burdening any other code that may be pulling this in.

    I think you need to move the abstraction down a level to avoid parameterizing your public Msg type. I would suggest having four concrete constructors for Msg instead of parameterizing it, and shift the abstraction down to a helper LoadInfo a type:

    type alias LoadInfo a =
        { worker : Worker a
        , url : Url
        , result : Result Http.Error ( Int, List a )
        }
    
    type Msg
        = LoadPost (LoadInfo Post)
        | LoadCategory (LoadInfo Category)
        | LoadTag (LoadInfo Tag)
        | LoadAuthor (LoadInfo Author)