Module exposing only some messages

Hi, I’m trying to implement a pattern that @rupert mentioned a couple of times.

My module handles some http requests with the server, I called it “Loader” and this is its signature:

module Loader exposing (init, Model, Msg, Out(..), update)

Then I created two kind of messages, internals (Msg) and externals (Out):

type Out                                                                                                                                                                                                                                     
  = GotSeriesOut Model                                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
type Msg                                                                                                                                                                                                                                     
    = BatchFinished                                                                                                                                                                                                                          
    | GotImages DicomTypes.DicomSeries (Result Http.Error (List DicomTypes.DicomImage))                                                                                                                                                      
    | GotSeries (Result Http.Error (List DicomTypes.DicomSeries))

The update is something like:

update : Msg -> Model -> ( Model, Cmd Msg, Maybe Out )                                                                                                                                                                                       
update msg model =                                                                                                                                                                                                                           
    case msg of                                                                                                                                                                                                                              
        GotImages dicomseries result ->                                                                                                                                                                                                      
          case result of                                                                                                                                                                                                                     
            Ok images ->         
                -- code removed                                                                                                                                                                                                 
                                                                                                                                                                                                                                             
        GotSeries result ->                                                                                                                                                                                                                                                                                                                                                                                                                  
          case result of                                                                                                                                                                                                                     
            Ok series ->                                                                                                                                                                                                                     
                -- code removed                                                                                                                                                                                                                                                                                                                          
              ({model | dicomSeries = series}, cmds, Just (GotSeriesOut model))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
        BatchFinished ->                                                                                                                                                                                                                     
              (model, Cmd.none, Nothing)

As you can see, when a GotSeries Msg is catched by the Loader’s update function, it fires a GotSeriesOut, that must be captured by the Main module.

In the Main module I had to define two messages, one for Loader.Out and other for Loader.Msg (please tell me if Loader.Msg is really needed).

type Msg                                                                                                                                                                                                                                     
    -- code removed                                                                                                                                                                                                         
    | LoaderMsg Loader.Msg                                                                                                                                                                                                                   
    | LoaderOut Loader.Out

In the Model I also had to add a reference to Loader.Model:

type alias Model =                                                                                                                                                                                                                           
    { -- code removed                                                                                                                                                                                              
    , loaderModel : Loader.Model                                                                                                                                                                                                             
    }

Now my problem, the update function.

I needed to catch the Loader.Msg (again, please tell me if there is a way to avoid this) as this:

        LoaderMsg loaderMsg ->                                                                                                                                                                                                               
            let                                                                                                                                                                                                                              
              (loaderModel, loaderCmd, loaderOut) =                                                                                                                                                                                          
               Loader.update loaderMsg model.loaderModel                                                                                                                                                                                     
                                                                                                                                                                                                                                             
            in                                                                                                                                                                                                                               
            ( { model | loaderModel = loaderModel, series = loaderModel.dicomSeries }                                                                                                                                                        
            , Cmd.map LoaderMsg loaderCmd                                                                                                                                                                                                    
            )

Then, as I just want to catch Out messages I implemented this:

        LoaderOut loaderOut ->                                                                                                                                                                                                               
            case loaderOut of                                                                                                                                                                                                                
                Loader.GotSeriesOut m ->                                                                                                                                                                                                     
                    let                                                                                                                                                                                                                      
                       (loaderModel, loaderCmd, loaderOutCmd) =                                                                                                                                                                              
                            Loader.update loaderOut model.loaderModel                                                                                                                                                                        
                                                                                                                                                                                                                                             
                       _ =                                                                                                                                                                                                                   
                            Debug.log "LoaderOout" m                                                                                                                                                                                         
                    in                                                                                                                                                                                                                       
                        ( { model | loaderModel = m, series = m.dicomSeries }                                                                                                                                                                
                        , Cmd.map LoaderOut loaderOutCmd

Btw, the above code doesn’t work, I get this error:

This `loaderOutCmd` value is a:

    Maybe Loader.Out

But `map` needs the 2nd argument to be:

    Cmd Loader.Out

I think I mentioned the pattern you describe as something that I would avoid doing - Out Messages -

Anyway, to turn your Maybe Loader.Out into a Cmd Loader.Out you need to do:

message : Maybe msg -> Cmd msg
message x =
    case x of 
        Just val ->
            Task.perform identity (Task.succeed val)

        Nothing ->
            Cmd.none


update msg model =
        ...
        LoaderOut loaderOut ->                                                                                                                                                                                                               
            case loaderOut of                                                                                                                                                                                                                
                Loader.GotSeriesOut m ->                                                                                                                                                                                                     
                    let                                                                                                                                                                                                                      
                       (loaderModel, loaderCmd, maybeLoaderOut) =                                                                                                                                                                              
                            Loader.update loaderOut model.loaderModel                                                                                                                                                                                                                                                                                                                                                                                                                     
                    in                                                                                                                                                                                                                       
                        ( { model | loaderModel = m, series = m.dicomSeries }                                                                                                                                                                
                        , Cmd.batch [Cmd.map LoaderMsg loaderCmd,  maybeLoaderOut |> message |> Cmd.map LoaderOut ])

Instead of using out messages, I now greatly prefer to use the so-called - Config Pattern -

Although, I prefer to call it the - Action Pattern - Since I sometimes have a record called Config that I set a bunch of flags or whatever in when intializing something. Actions seems to describe the definition of handlers for outputs better than Config to me.

Out messages are a form of defunctionalization. Defunctionalization is where you capture the arguments of a function as data for later evaluation, usually as a custom type. The Out type is just such a custom type that defunctionalizes the different outputs your module can generate.

It is more flexible, as well as more in the functional style, to use higher order functions instead. Define the set of outputs your module can generate, from either its update or view functions, as functions themselves. Represent these as a record that the caller must provide, in order to call-back on the output functions, so that the caller can provide implementations of these. This makes a way that the caller plugs-in to the module by implementing behaviour that is handled outside of it, in response to certain events going on inside of it.

I claim that this way is more flexible than the out message pattern, because you could implement the Actions by defining an out message type, and using its constructors as the implementation of the Actions callbacks. The higher-order function pattern can always be defunctionalized if the caller really wants to. If the implementation has already defunctionalized, the caller cannot opt to refunctionalize without re-implementing the code it is trying to call.

In this example, we say that the update and view functions are higher order, because they take some other functions as inputs.

module Loader exposing (Msg, Model, Actions, update, view)

type Msg
   = ...

type alias Model =
   ...

type alias Actions msg =
    { toSelf : Msg -> msg
    , gotSeries : Model -> msg
    }

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

view : Actions msg -> Model -> Html msg

The toSelf function is used to wrap Loader.Msg to the callers Msg type. gotSeries is used to call-back when the gotSeries event occurs.

I would only pass the Actions to update or view if it really needs them. Often only the view will need them, and can pass outputs directly to the caller without going through the Loader.update.

An advantage of this is that you now do not need to use Task.perform. In fact, you already did not need to use it, you could have evaluated the Maybe Loader.Out immediately, without turning it into a LoaderOut message. Task.perform can cause annoying delays in processing messages, since it goes back through the Elm runtime loop, and might miss a screen repaint as a result.

Also you no longer have to define an Out type as you are no longer defunctionalizing.

Another plus of this way, is that I find it tends to discourage doing ellaborate things with routing out messages around an application - actor model style. Elm is single threaded and does not need the actor model way of thinking and suffers badly from excess boilerplate if you do try to use it. Do not think of a large Elm program as being made of communicating modules; its just adding complexity to your code that does not need to be there.

1 Like

Fun exercise for the reader - Take some small Elm program you wrote and refunctionalize its Msg type to:

type alias Msg =
    Model -> (Model, Cmd Msg)

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

Thanks @rupert for the long-detailed answer. I’ll read it carefully and try to adapt my program to your suggestions.

Sorry @rupert I’m trying to understand how the Actions are called from the Main program and cannot figure out how this should be done.

Can you show that with an example?

Does this make it clear?

module Main exposing (..)

import Loader

type alias Model =                                                                                                                                                                                                                           
    { ...
    , loaderModel : Loader.Model                                                                                                                                                                                                             
    }

type Msg
    = LoaderMsg Loader.Msg -- Need to wrap the Loader msg to connect them up to its update.
    | GotSeries Loader.Model -- Set up actions to create this when the series is available.
    | ...

loaderActions : Loader.Actions
loaderActions = 
    { toSelf = LoaderMsg
    , gotSeries = \loaderModel -> GotSeries loaderModel
    }

update msg model = 
    case msg of 
        LoaderMsg loaderMsg ->
            let                                                                                                                                                                                                                              
                (loaderModel, loaderCmd) =                                                                                                                                                                                          
                    Loader.update loaderActions loaderMsg model.loaderModel                                                                                                                                                                                                                                                                                                                                                                                                                                  
            in                                                                                                                                                                                                                               
            ( { model | loaderModel = loaderModel }                                                                                                                                                        
            , Cmd.map LoaderMsg loaderCmd                                                                                                                                                                                                    
            )

        GotSeries loaderModel ->
            ... -- Do something with the series output

Yes!, now I get it. Thank you.

@rupert I thought that would work, but I get this:

The `Actions` type needs 1 argument, but I see 0 instead:

100| loaderActions : Loader.Actions
                     ^^^^^^^^^^^^^^
What is missing? Are some parentheses misplaced?

Oops! It should be:

loaderActions : Loader.Actions Msg

Its Msg for the type parameter, since these actions map the Loader.Msg onto Main.Msg.

BTW, Evan provided this example for sortable tables that uses the Config Pattern:

Its really worth a look at since it avoids having a Msg and update in the Table module at all (its model is called State). Changes to the table state are provided back to the caller, and are implemented as functions within the view code in the Table module. Its not immediately obvious how this works, but if you look at the onClick function at line 476 that is where new states are being built.

This avoids the nested TEA structure that we too often reach for, and is a nice example showing how Msg and update can be refunctionalized when compared with the nested TEA approach.

1 Like

Hi again @rupert, now the code compiles, but I’m not getting the GotSeries message from Main. I do get the LoaderMsg, which contains GotSeries and GotImages, but GotSeries is never captured by Main update.

Here’s the last part of my update (from Main.elm):


        LoaderMsg loaderMsg ->                                                                                                                                                                                                               
            let                                                                                                                                                                                                                              
                _ = Debug.log "LoaderMsg" loaderMsg                                                                                                                                                                                          
                (loaderModel, loaderCmd) =                                                                                                                                                                                                   
                    Loader.update loaderActions loaderMsg model.loaderModel                                                                                                                                                                  
            in                                                                                                                                                                                                                               
            ( { model | loaderModel = loaderModel }                                                                                                                                                                                          
            , Cmd.map LoaderMsg loaderCmd                                                                                                                                                                                                    
            )                                                                                                                                                                                                                                
                                                                                                                                                                                                                                             
        GotSeries loaderModel ->                                                                                                                                                                                                             
            let                                                                                                                                                                                                                              
                _ = Debug.log "GotSeries" loaderModel                                                                                                                                                                                        
            in                                                                                                                                                                                                                               
            ( model, Cmd.none )

You must need to use .gotSeries from the actions somewhere in the Loader update? What does the code for that look like?

        GotSeries result ->                                                                                                                                                                                                                  
          -- Se recibe la lista de series                                                                                                                                                                                                    
          case result of                                                                                                                                                                                                                     
            Ok series ->                                                                                                                                                                                                                     
              let                                                                                                                                                                                                                            
                _ = Debug.log "GotSeries.series" series                                                                                                                                                                                      
                cmds = Cmd.batch (List.map (\k -> getImages k) series)                                                                                                                                                                       
                m = actionsMsg.gotSeries model      -- something like this?                                                                                                                                                                                         
              in                                                                                                                                                                                                                             
              ({model | dicomSeries = series}, cmds )

Should the result of actionMsg.gotSeries model be part of the cmds array?

Yes, and in this case you would need to use Task.perform to make it into a Cmd.

message : msg -> Cmd msg
message x =
    Task.perform identity (Task.succeed val)

...

    Ok series ->                                                                                                                                                                                                                     
        let                                                                                                                                                                                                                            
            _ = Debug.log "GotSeries.series" series                                                                                                                                                                                      
            imageCmds = Cmd.batch (List.map (\k -> getImages k) series)                                                                                                                                                                       
            nextModel = {model | dicomSeries = series}
            seriesCmd = actionsMsg.gotSeries nextModel |> message
        in                                                                                                                                                                                                                             
        (model, Cmd.batch [seriesCmd, imagesCmds] )

I have to admit doing it this ways made using Task.perform necessary, and the out message style could have avoided it. I only find Task.perform is a problem for making user interaction a little bit laggy though - for example, if you had a drawing program and the mouse actions somehow got passed through a Task.perform you would notice the drawing actions on screen being laggy - it should not be a problem here.

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