brian-watkins / elm-procedure / Procedure

Orchestrate commands, subscriptions, and tasks.


type alias Procedure e a msg =
Internal.Procedure e a msg

Represents some sequence of commands, subscriptions, or tasks that ultimately results in some value.

Execute a Procedure

run : (Internal.Msg msg -> msg) -> (a -> msg) -> Procedure Basics.Never a msg -> Platform.Cmd.Cmd msg

Execute a procedure that cannot fail.

Note: The first argument tags a Procedure.Program.Msg as a Msg in your application. The second argument tags the value produced by the Procedure as a Msg in your application.

try : (Internal.Msg msg -> msg) -> (Result e a -> msg) -> Procedure e a msg -> Platform.Cmd.Cmd msg

Execute a procedure that may fail.

Note: The first argument tags a Procedure.Program.Msg as a Msg in your application. The second argument tags the result of the Procedure as a Msg in your application.

Basic Procedures

provide : a -> Procedure e a msg

Generate a procedure that simply provides a value.

Procedure.provide "Hello!"
  |> Procedure.run ProcedureTagger StringTagger

This will result in StringTagger "Hello".

fetch : ((a -> msg) -> Platform.Cmd.Cmd msg) -> Procedure e a msg

Generate a procedure that gets the value produced by executing some Cmd.

For example, if you wanted to have the user select a file and then convert that to a string, you could do the following:

Procedure.fetch (File.Select.file ["application/zip"])
  |> Procedure.andThen (\file -> 
    File.toString file
      |> Procedure.fromTask
  )
  |> Procedure.run ProcedureTagger StringTagger

Note that only some commands produce a value directly. To execute those commands that do not produce a value, use do.

fetchResult : ((Result e a -> msg) -> Platform.Cmd.Cmd msg) -> Procedure e a msg

Generate a procedure that gets the result produced by executing some Cmd.

For example, if you wanted to make an Http request and then map the response, you could do the following:

Procedure.fetchResult (\tagger ->
  Http.get
    { url = "http://fun.com/fun.html"
    , expect = Http.expectString tagger
    }
  )
  |> Procedure.map (\words ->
    "Fun stuff: " ++ words
  )
  |> Procedure.catch (\error ->
    Procedure.provide "No Response"
  )
  |> Procedure.run ProcedureTagger StringTagger

If the Http request fails, then the result will be: No response.

collect : List (Procedure e a msg) -> Procedure e (List a) msg

Generate a procedure that collects the results of a list of procedures.

Procedure.collect
  [ Procedure.provide "One"
  , Procedure.provide "Two"
  , Procedure.provide "Three"
  ]
  |> Procedure.run ProcedureTagger ListStringTagger

Then the result would be ListStringTagger [ "One", "Two", "Three" ].

fromTask : Task e a -> Procedure e a msg

Generate a procedure that runs a task.

For example, if you wanted to have the user select a file and then convert that to a string, you could do the following:

Procedure.fetch (File.Select.file ["application/zip"])
  |> Procedure.andThen (\file ->
    File.toString file
      |> Procedure.fromTask
  )
  |> Procedure.run ProcedureTagger StringTagger

If the task fails, the procedure will break at this point, just as if Procedure.break had been used.

break : e -> Procedure e a msg

Generate a procedure that breaks out of the current procedure.

You can use this to stop a procedure early:

Procedure.fetch (File.Select.file ["text/plain"])
  |> Procedure.andThen (\file ->
    File.toString file
      |> Procedure.fromTask
  )
  |> Procedure.andThen (\text ->
    if String.length text > 100 then
      Procedure.break "File is too long!"
    else
      Procedure.provide text
  )
  |> Procedure.map (\text -> doSomethingWithTheText text)
  |> Procedure.try ProcedureTagger StringResultTagger

where StringResultTagger tags a Result String String. If the break is triggered, then the result would be Err "File is too long!".

do : Platform.Cmd.Cmd msg -> Procedure Basics.Never () msg

Generate a procedure that executes a Cmd that does not produce any value directly.

Use do to execute port functions that generate Cmd values.

Procedure.do myFunPortCommand
  |> Procedure.map (\_ -> "We did it!")
  |> Procedure.run ProcedureTagger StringTagger

If you want to send a port command and expect a response later via some subscription, use a Channel.

endWith : Platform.Cmd.Cmd msg -> Procedure Basics.Never Basics.Never msg

Generate a procedure that runs a command and states that no further value will be provided, effectively ending the procedure.

Use this function when you need to end a procedure with a command that produces no message directly, such as a port command, and you don't want to add a NoOp case to your update function.

For example, this procedure gets the current time and sends it out via a port (called sendMillisOut in this case), and never produces a message for the update function.

Procedure.fromTask Task.now
  |> Procedure.map Time.posixToMillis
  |> Procedure.andThen (\millis ->
    Procedure.endWith <| sendMillisOut millis
  )
  |> Procedure.run ProcedureMsg never

Build a Procedure

andThen : (a -> Procedure e b msg) -> Procedure e a msg -> Procedure e b msg

Generate a new procedure based on the result of the previous procedure.

Procedure.provide "An awesome value"
  |> Procedure.andThen (\data -> 
    Procedure.provide <| data ++ "!!!"
  )
  |> Procedure.run ProcedureTagger StringTagger

Then the result would be StringTagger "An awesome value!!!".

catch : (e -> Procedure f a msg) -> Procedure e a msg -> Procedure f a msg

Generate a new procedure when some previous procedure results in an error, usually do to processing a break procedure.

For example, you could check the result of some data and then break to skip the next steps until you reach a catch step.

Procedure.fetch someCommand
  |> Procedure.andThen (\data ->
    if data.message == "Success" then
      Procedure.provide data
    else
      Procedure.break data.message
  )
  |> Procedure.andThen (\data ->
    Channel.send (somePortCommand data)
      |> Channel.receive somePortSubscription
      |> Channel.await
  )
  |> Procedure.catch (\errorData -> 
    Procedure.provide "Some default message"
  )
  |> Procedure.map (\data -> data ++ "!")
  |> Procedure.run ProcedureTagger StringTagger

If the data fetched via someCommand is deemed not successful, then the next steps will be skipped and the result of this procedure will be StringTagger "Some default message!".

Map the Output of a Procedure

map : (a -> b) -> Procedure e a msg -> Procedure e b msg

Generate a procedure that transforms the value of the previous procedure.

Procedure.collect
  [ Procedure.provide "One"
  , Procedure.provide "Two"
  , Procedure.provide "Three"
  ]
  |> Procedure.map (String.join ", ")
  |> Procedure.run ProcedureTagger StringTagger

Then the result would be StringTagger "One, Two, Three".

map2 : (a -> b -> c) -> Procedure e a msg -> Procedure e b msg -> Procedure e c msg

Generate a procedure that provides a new value based on the values of two other procedures.

Procedure.map2
  (\a b -> a ++ " AND " ++ b)
  (Procedure.provide "One")
  (Procedure.provide "Two")
  |> Procedure.run ProcedureTagger StringTagger

Then the result would be `StringTagger "One AND Two".

Note: map2 executes each procedure in order. The second procedure will be executed only if the first succeeds; if the first fails, then the whole procedure will fail. This follows the behavior of Task.map2 in the core library.

map3 : (a -> b -> c -> d) -> Procedure e a msg -> Procedure e b msg -> Procedure e c msg -> Procedure e d msg

Generate a procedure that provides a new value based on the values of three other procedures.

Note: map3 executes each procedure in order. See map2 for more details.

mapError : (e -> f) -> Procedure e a msg -> Procedure f a msg

Generate a procedure that transforms the error value of a previous procedure.

Procedure.provide "Start"
  |> Procedure.andThen (\_ -> Procedure.break "Oops")
  |> Procedure.mapError (\err -> err ++ "???")
  |> Procedure.try ProcedureTagger ResultTagger

Then the result would be Err "Oops???".

Note: Error values can be set explicitly by using break.