Elm. Comfortable and awkward. Http, Task

    We continue to talk about Elm 0.18 .

    Elm. Comfortable and awkward
    Elm. Comfortable and awkward. Composition
    Elm. Comfortable and awkward. Json.Encoder and Json.Decoder

    In this article we consider the issues of interaction with the server part.

    Query execution

    Examples of simple requests can be found in the description of the Http package .

    Request type - Http.Request a .
    The type of the query result is Result Http.Error a.
    Both types are parameterized by a custom type, the decoder of which must be specified when forming the request.

    You can execute the query using the functions:

    1. Http.send;
    2. Http.toTask.

    Http.send is allowed to execute the request and upon its completion passes the message to the update function specified in the first argument. The message carries information about the result of the request.

    Http.toTask allows you to create a Task from the request that you can perform. Using the Http.toTask function, in my opinion, is the most convenient, since Task instances can be combined with each other using various functions , such as Task.map2 .

    Consider an example. Suppose you need to run two consecutive dependent queries in order to save user data. Let it be the creation of a post from the user and the saving of photos to him (some CDN is used).

    First consider the implementation for the case of Http.Send. For this we need two functions:

    save : UserData -> Request Http.Error UserData
    save userData =
      Http.post “/some/url” (Http.jsonBody (encodeUserData userData)) decodeUserData
    saveImages : Int -> Images -> Request Http.Error CDNData
    saveImages id images =
      Http.post (“/some/cdn/for/” ++ (toString id)) (imagesBody images) decodedCDNData

    The types UserData and CDNData will not be described, for example they are not important. The encodeUserData function is an encoder. saveImages accepts a user data identifier that is used when generating an address, and a list of photos. The imagesBody function forms the request body of the multipart / form-data type . The decodeUserData and decodedCDNData functions decode the server response for user data and the result of the query to the CDN, respectively.

    Next we need two messages, the results of the query:

      = DataSaved (Result Http.Error UserData)
      | ImagesSaved (Result Http.Error CDNData)

    Suppose, somewhere in the implementation of the update function, there is a section of code that performs data saving. For example, it might look like this:

    update : Msg -> Model -> (Model, Cmd Msg)
    update msg modelcase Msg of
        ClickedSomething ->
          (model, Http.send DataSaved (save model.userData))

    In this case, a request is created and marked with a DataSaved message. Further, this message is accepted:

    update : Msg -> Model -> (Model, Cmd Msg)
    update msg modelcase Msg of
        DataSaved (Ok userData) ->
          ( {model | userData = userData}, Http.send  ImagesSaved (saveImages userData.id model.images))
        DataSaved (Err reason) ->
          (model, Cmd.None)

    In case of successful saving, we update the data in the model and call the request to save photos where we transfer the received user data identifier. The processing of the ImagesSaved message will be similar to DataSaved, it will be necessary to process successful and failed cases.

    Now consider the option to use the function Http.toTask. Using the described functions, we define a new function:

    saveAll : UserData -> Images -> Task Http.Error (UserData, CDNData)
    saveAll : userData images =
      save model.userData
        |> Http.toTask
        |> Task.andThen (\newUserData ->
          saveImages usersData.id images 
            |> Http.toTask
            |> Task.map (\newImages -> 
               (userData, newImages)

    Now using the ability to combine tasks, we can get all the data in one message:

      = Saved (Result Http.Error (UserData, CDNData))
    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model
      case Msg of
        ClickedSomething ->
          (model, Task.attempt Saved (saveAll model.userData model.images))
       DataSaved (Ok (userData, images)) ->
          ( {model | userData = userData, images = images}, Cmd.none)
        DataSaved (Err reason) ->
          (model, Cmd.None)

    To execute queries, use the Task.attempt function , which allows you to perform a task. Not to be confused with the Task.perform function . Task.perform - allows you to perform tasks that can not fail . Task.attempt - performs tasks that may fail .

    This approach is more compact in terms of the number of messages, the complexity of the update function, and allows you to keep the logic more local.

    In my projects, in applications and components, I often create a module Commands.elm, in which I describe the functions of interacting with the server part with the type ... -> Task Http.Error a.

    Query execution status

    In the process of executing requests, the interface often has to be blocked in whole or in part, and also to report errors in the execution of requests, if any. In general, the status of the request can be described as:

    1. Request failed;
    2. the request is executed;
    3. the request was successful;
    4. request failed.

    For such a description there is a package RemoteData . At first, he actively used it, but over time, the availability of an additional type of WebData became redundant, and working with it was tedious. The following rules appeared instead of this package:

    1. to declare all data from the server with the Maybe type. In this case, Nothing indicates a lack of data;
    2. declare an int type loading attribute in an application or component model. The parameter stores the number of queries executed. The only disadvantage of this approach is the need to increment and decrement the attribute at the beginning of the request and upon completion, respectively;
    3. declare in the application model or components an attributes attribute of the List String type. This attribute is used to store error data.

    The scheme described is not much better than the version with the RemoteData package, as practice shows. If someone has other options, share in the comments.

    The status of the request should include the download progress from the Http.Progress package .

    Sequence of tasks

    Consider options for sequences of tasks that are often found in the development:

    1. sequential dependent tasks;
    2. consecutive independent tasks;
    3. parallel independent tasks.

    Sequential dependent tasks have already been considered above, in this section I will give a general description and approaches to implementation.

    The sequence of tasks is interrupted at the first failure and an error is returned. If successful, a combination of results is returned:

      |> Task.andThen (\resultA ->
          |> Task.map (\resultB ->
            (resultA, resultB)

    This code creates a task of type Task error (a, b), which can be executed later.

    The Task.andThen function allows you to submit a new task for execution in case of successful completion of the previous one. The Task.map function allows you to convert the melon results of the execution in case of success.

    There are options when successful completion of the task will not be enough and you need to check the consistency of the data. Assume that user IDs match:

      |> Task.andThen (\resultA ->
          |> Task.andThen (\resultB ->
            case resultA.userId == resultB.userId ofTrue -> 
                Task.succeed (resultA, resultB)
              False -> 
                Task.fail “Userisnot the same”

    It is worth noting that instead of the Task.map function, the Task.andThen function is used, and the success of the second task is determined independently using the Task.succeed and Task.fail functions .

    If one of the tasks may fail and this is acceptable, then you need to use the Task.onError function to specify the value in case of an error:

      |> Task.onError (\msg -> Task,succeed defaultValue)
      |> Task.andThen (\resultA ->
          |> Task.map (\resultB ->
            (resultA, resultB)

    The call to Task.onError should be declared immediately after the task is declared.

    Sequential independent requests can be performed using Task.mapN functions. Which allow you to combine several results of tasks into one. The first dropped task interrupts the execution of the entire chain, so for default values ​​use the Task.onError function. Also check out the Task.sequence function , it allows you to perform a series of similar tasks.

    Parallel tasks in the current implementation of the language are not described. Their implementation is possible at the application or component level through event handling in the update function. All logic remains on the shoulders of the developer.

    Also popular now: