Best way to design a web app which status depend on the success of several http requests?

New to Elm, I am discovering and loving the way it handles an application status with this design:

type Status
= Loading
| Success Data
| Error String

The side project I am working on as a model that needs to be created out of 3 different JSON response… what is the best design for this purpose? I started doing something like this:

type Status
= Loading
| Success Data1 Data2
| Error String

Data1 and Data2 are 2 records, created from JSON Decoders from different API calls, that basically I need to compare and present the diff to the user. That is why I am thinking that the app can only be in the Success state if both API calls have been successful, and both decoding too… so I instinctively attached them both the the Sucess constructor.

But I am not sure its the best solution, as I can’t figure out how to update the model correctly. The compiler complains about my update function and me using ‘_’ here. I can’t figure out why, but it feels wrong:

update msg model =
case msg of
GotData1 (Ok data1) ->
( { model | status = Success data1 _ }, Cmd.none )

    GotData1 (Err message) ->
        ( { model | status = Error (errorToString message) }, Cmd.none )

    GotData2 (Ok windDirection) ->
        ( { model | status = Success _ data2 }, Cmd.none )

    GotData2 (Err message) ->
        ( { model | status = Error (errorToString message) }, Cmd.none )

You need to use Http.task to join your two requests together


type Msg 
    = GetData 
    | GotData Status 
    ...

update msg model = 
    case msg of 
        GetData -> 
            let 
               first = 
                   Http.task { method = "GET", ...}
               second = 
                   Http.task { method = "GET", ...}

               request = 
                   Task.map2 Success first second
                       |> Task.onError (\e -> Task.succeed (Error (httpErrorToString e)))
                       |> Task.perform GotData
           in 
           (model, request) 

        GotData status -> 
            ({model | status = status} , Cmd.none) 
        ....
3 Likes

Alternative idea - ModelStatus stays at Loading until either request returns error or both succeed.
The Ok cases could probably be simplified using some helper function.

type ModelStatus
    = Loading (Maybe Data1) (Maybe Data2)
    | Success Data1 Data2
    | Error String

update msg model =
    case msg of
        GotData1 (Ok data1) ->
            case model.status of
                Loading Nothing (Just data2) ->
                    ( { model | status = Success (Just data1) (Just data2) }, Cmd.none )
                Loading Nothing Nothing ->
                    ( { model | status = Loading (Just data1) Nothing }, Cmd.none )
                Error _ ->
                    ( model, Cmd.none )
                Success _ _ ->
                    ( { model | status = Error "..." }, Cmd.none )

        GotData1 (Err message) ->
            ( { model | status = Error (errorToString message) }, Cmd.none )

        GotData2 (Ok data2) ->
            case model.status of
                Loading (Just data1) Nothing ->
                    ( { model | status = Success (Just data1) (Just data2) }, Cmd.none )
                Loading Nothing Nothing ->
                    ( { model | status = Loading Nothing (Just data2) }, Cmd.none )
                Error _ ->
                    ( model, Cmd.none )
                Success _ _ ->
                    ( { model | status = Error "..." }, Cmd.none )

        GotData2 (Err message) ->
            ( { model | status = Error (errorToString message) }, Cmd.none )
1 Like

Or you could have a non-determinstic state machine:

type ModelStatus
    = Loading
    | Part1 Data1
    | Part2 Data2
    | Success Data1 Data2
    | Error String

And transition to Part1 or Part2 from Loading, depending on which result arrives first.

1 Like

The others have already presented ways of solving this problem, so let me say something about the compiler’s complaint: The special argument _ can only be used inside of a pattern (i.e. in a function argument or on the left side of -> within a case expression). It means: I will not use this part of the value on the right-hand side, so I don’t want to give it a name. You might think of it as “I don’t care about this value” but this might lead you to the idea of using in other places as well, for example as the argument to a constructor with the idea that you don’t want to specify this argument. But the compiler would still need to provide a value; after all, in another part of your code you might try to use this value and we don’t want our program to crash unexpectedly. Unfortunately, the compiler cannot magically guess a good value (why should it be able to if you can’t provide a value yourself); therefore, this is not allowed.

In other programming languages, you might use null in such a case, but in Elm, you need to specify that you want your value to be nullable: This is the Maybe type used above. Once you have a Maybe somewhere, the compiler will force you to check that it is not Nothing (that’s our name for null) in the appropriate places. This can become quite annoying, so it is often a good idea to restrict your Maybes to only those parts of your program that actually need them. Look at @malaire’s solution above: While the model is Loading, the data is stored within a Maybe but at soon as all parts of the data are there, Loading is replaced by Success where Maybes no longer occur. The strictness of Elm’s compiler will make sure that you don’t accidentally switch to success before you can provide all parts of the data. (It will not protect you against the inverse mistake where you forget to switch to Success once all the data has arrived, though.)

1 Like

If it helps, here’s an example of how I typically handle this: https://github.com/ianmackenzie/elm-3d-scene/blob/master/examples/TexturedSphere.elm. That’s actually an example I created for elm-3d-scene, but the first chunk of the example (up to line 217) is mostly just about loading three separate textures in parallel via HTTP (all the actual 3D rendering code comes afterwards, which you can ignore).

Specifically, check out the Model type, init, and the Loading case of the update function. Like @malaire suggested, I do in fact have a checkIfLoaded helper function which checks to see if a bunch of Maybe values are all Justs and transitions to the Loaded state if that’s the case (staying in the Loading state otherwise).

Using Http.task and Task.map3 could also work and would simplify the logic, but would also mean that the HTTP requests would be made in serial instead of parallel, increasing the total load time.

3 Likes

In this kind of data fetching in a parallel way, elm-multi-waitable can also be a solution to do something after all things waited are resolved.

https://package.elm-lang.org/packages/IzumiSy/elm-multi-waitable/latest/

This package abstracts multiple Maybe types that usually fills up your Model strucutre with more readable variants like below.

type Model 
    = Loading (MultiWaitable.Wait3 Msg User Options Bookmarks)
    | Loaded User Options Bookmarks

This more advantageous thing is that, elm-multi-waitable does not care the things to wait are parallel or sequential. This package only covers waiting state, so how to update state is totally controllable.

2 Likes

Thanks, I like your example a lot.
I try to implement it in my code, but my real life code has actually 5 pieces of data total, with 4 different HTTP request.
As I can’t squeeze 5 items in a tuple I have to rewrite a little checkIfLoaded. Elm suggests to use a record using of a tuple… how does pattern matching work on records? I tried different things found online but no success so far.

`I am stuck here:

    checkIfLoaded :
        { surfSpots : Maybe (List SurfSpot)
        , swellDirection : Maybe (List Direction)
        , windDirection : Maybe Direction
        , surfHeight : Maybe ( Int, Int )
        , tide : Maybe Tide
        }
        -> Model
    checkIfLoaded ({surfSpots, swellDirection, windDirection, surHeight, tide } as data) =
        case data of
            { Just surfSpots
            , Just swellDirection
            ,Just windDirection
            , Just surfHeight
            , Just tide } ->
                Success
                    { surfSpots = surfSpots
                    , swellDirection = swellDirection
                    , windDirection = windDirection
                    , surfHeight = surfHeight
                    , tide = tide
                    }

            _ ->
                Loading data

The error is of type UNFINISHED RECORD PATTERN.
Thanks again for the precious help.

Unfortunately, you can’t do nested pattern matches on records in Elm and the compiler error is not helpful in this case. (AFAIK: Someone please tell me if I’m wrong.)

I think there are two ways to handle this case: You can either nest your pattern matches like so (with only three fields for brevity):

checkIfLoaded :
    { surfSpots : Maybe (List SurfSpot)
    , swellDirection : Maybe (List Direction)
    , windDirection : Maybe Direction
    }
    -> Model
checkIfLoaded partialData =
    case partialData.surfSpots of
        Just surfSpots ->
            case partialData.swellDirection of
                Just swellDirection ->
                    case partialData.windDirection of
                        Just windDirection of ->
                            Success
                                { surfSpots = surfSpots
                                , swellDirection = swellDirection
                                , windDirection = windDirection
                                }

                        Nothing ->
                            Loading partialData

                Nothing ->
                    Loading partialData

        Nothing ->
            Loading partialData

Or you can use nested tuples:

checkIfLoaded :
    { surfSpots : Maybe (List SurfSpot)
    , swellDirection : Maybe (List Direction)
    , windDirection : Maybe Direction
    , surfHeight : Maybe ( Int, Int )
    , tide : Maybe Tide
    }
    -> Model
checkIfLoaded partialData =
    case ( partialData.surfSpots, partialData.swellDirection, ( partialData.windDirection, partialData.surfHeight, partialData.tide ) ) of
        ( Just surfSpots, Just swellDirection, ( Just windDirection, Just surfHeight, Just tide ) ) ->
             Success
                 { surfSpots = surfSpots
                 , swellDirection = swellDirection
                 , windDirection = windDirection
                 , surfHeight = surfHeight
                 , tide = tide
                 }
        _ ->
           Loading partialData

(I think there is also a third solution of building something similiar to Decode.map2 etc, but I haven’t thought it through.)

Maybe.mapX functions go up to map5, so maybe this (not tested):

checkIfLoaded :
    { surfSpots : Maybe (List SurfSpot)
    , swellDirection : Maybe (List Direction)
    , windDirection : Maybe Direction
    , surfHeight : Maybe ( Int, Int )
    , tide : Maybe Tide
    }
    -> Model
checkIfLoaded partialData =
    Maybe.map5
        (\surfSpots swellDirection windDirection surfHeight tide ->
             Success
                 { surfSpots = surfSpots
                 , swellDirection = swellDirection
                 , windDirection = windDirection
                 , surfHeight = surfHeight
                 , tide = tide
                 }
        )
        partialData.surfSpots
        partialData.swellDirection
        partialData.windDirection
        partialData.surfHeight
        partialData.tide
        |> Maybe.withDefault (Loading partialData)
2 Likes

Actually, I did think about it: You can define

andMap : Maybe a -> Maybe (a -> b) -> Maybe b
andMap = Maybe.map2 (|>)

(or import the elm-community/maybe-extra package) and then do

checkIfLoaded :
    { surfSpots : Maybe (List SurfSpot)
    , swellDirection : Maybe (List Direction)
    , windDirection : Maybe Direction
    , surfHeight : Maybe ( Int, Int )
    , tide : Maybe Tide
    }
    -> Model
checkIfLoaded partialData =
    Just (\surfSpots swellDirection windDirection surfHeight tide ->
             Success
                 { surfSpots = surfSpots
                 , swellDirection = swellDirection
                 , windDirection = windDirection
                 , surfHeight = surfHeight
                 , tide = tide
                 }
        )
    |> andMap partialData.surfSpots
    |> andMap partialData.swellDirection
    |> andMap partialData.windDirection
    |> andMap partialData.surfHeight
    |> andMap partialData.tide
    |> Maybe.withDefault (Loading partialData)

(This is essentially the same as @malaire’s solution, but extendable beyond 5 parts.)

1 Like

Hey @Yannick971, apologies for the delay - meant to reply to this earlier and totally forgot! One way I sometimes handle cases like this is by defining a little custom type that can handle more than three values, for example:

type FiveMaybes a b c d e
    = FiveMaybes (Maybe a) (Maybe b) (Maybe c) (Maybe d) (Maybe e)

checkIfLoaded :
    { surfSpots : Maybe (List SurfSpot)
    , swellDirection : Maybe (List Direction)
    , windDirection : Maybe Direction
    , surfHeight : Maybe ( Int, Int )
    , tide : Maybe Tide
    }
    -> Model
checkIfLoaded data =
    case FiveMaybes data.surfSpots data.swellDirection data.windDirection data.surfHeight data.tide of
        FiveMaybes (Just surfSpots) (Just swellDirection) (Just windDirection) (Just surfHeight) (Just tide) ->
            Success
                 { surfSpots = surfSpots
                 , swellDirection = swellDirection
                 , windDirection = windDirection
                 , surfHeight = surfHeight
                 , tide = tide
                 }

        _ ->
            Loading data

(That code is from memory and may have typos, but hopefully the general idea is clear.)