arowM / elm-thread / Thread.Procedure

Core


type Procedure memory event

Procedures to be processed in a thread.

batch : List (Procedure memory event) -> Procedure memory event

Batch Procedures together. The elements are evaluated in order.


type ThreadId

An identifier for a thread.

stringifyThreadId : ThreadId -> String

Convert ThreadId into String.

Different ThreadIds will be converted to different strings, and the same ThreadIds will always be converted to the same string.


type alias Block memory event =
ThreadId -> List (Procedure memory event)

An alias for a bunch of Procedures.

Constructors

none : Procedure memory event

Construct a Procedure instance that do nothing.

modify : (memory -> memory) -> Procedure memory event

Construct a Procedure instance that modifies shared memory state.

Note that the update operation, passed as the first argument, is performed atomically. It means the state of shared memory read by a particular thread with modify is not updated by another thread until it is updated by the thread.

push : (memory -> Platform.Cmd.Cmd event) -> Procedure memory event

Construct a Procedure instance that issues a Cmd directed to the thread on which this function is evaluated.

await : (event -> memory -> List (Procedure memory event)) -> Procedure memory event

Construct a Procedure instance that awaits the local events for the thread.

If it returns empty list, it awaits again. Otherwise, it evaluates the given Procedure.

Note1: The shared memory state passed to the first argument function may become outdated during running the thread for the Procedure generated by that function, so it is safe to use this shared memory state only to determine whether to accept or miss events.

Note2: Technically, all the modify push async written before the await will be executed internally as a single operation. This avoids the situation where a local event triggered by a push occurs while processing tons of subsequent modifys and pushs, thus ensuring that the await always can catch the local event caused by the previous Procedures.

Note3: pushs written before an await will not necessarily cause local events in the order written. For example, if the first push sends a request to the server and it fires a local event with its result, and the second push sleeps for 0.1 seconds and then returns a local event, the first local event can fire later if the server is slow to respond. To avoid this situation, after using one push, catch it with await and use the next push, or use sync.

async : Block memory event -> Procedure memory event

Construct a Procedure instance that evaluates the given Block in the asynchronous thread.

The asynchronous thread is provided new ThreadId and runs independently of the original thread; therefore the subsequent Procedures in the original thread are evaluated immediately, and the asynchronous thread is cancelled when the original thread ends.

Infinite recursion by giving itself as the argument to async is not recommended to prevent threads from overgrowing. Use jump if you want to create threads that never end.

block : Block memory event -> Procedure memory event

Construct a Procedure instance that wait for the given Procedure to be completed.

Given Procedure is evaluated in the independent threads with new ThreadId, but the subsequent Procedures in the original thread are not evaluated immediately. For example, the following sleep function uses block to scope the WakeUp event so that it only affects the inside of the sleep function.

import Process
import Task
import Thread.Procedure as Procedure exposing (Procedure)

sleep : Float -> Procedure Memory Event
sleep msec =
    Procedure.block <|
        \_ ->
            [ Procedure.push <|
                \_ ->
                    Process.sleep msec
                        |> Task.perform (\() -> WakeUp)
            , Procedure.await <|
                \event _ ->
                    case event of
                        WakeUp ->
                            [ Procedure.none
                            ]

                        _ ->
                            []
            ]

Infinite recursion by giving itself as the argument to async is not recommended to prevent threads from overgrowing. Use jump if you want to create threads that never end.

sync : List (Block memory event) -> Procedure memory event

Construct a Procedure instance that wait for all the given Blocks to be completed.

Each Block is evaluated in the independent threads with its own ThreadId, but the subsequent Procedures in the original thread are not evaluated immediately, but wait for all the given Blocks to be completed.

race : List (Block memory event) -> Procedure memory event

Construct a Procedure instance that wait for one of the given Blocks to be completed.

Each Block is evaluated in the independent thread with its own ThreadId, but the subsequent Procedures in the original thread are not evaluated immediately, but wait for one of the given Blocks to be completed.

Note1: If one of the threads exits, all other threads will be suspended after processing until the next await.

quit : Procedure memory event

Quit the thread immediately.

Subsequent Procedures are not evaluated and are discarded.

jump : Block memory event -> Procedure memory event

Ignore subsequent Procedures, and evaluate given Block in the current thread. It is convenient for following two situations.

Make recursive Block

Calling itself in the Block will result in a compile error; The jump avoids it to makes the recursive Block.

import Thread.Procedure as Procedure exposing (Block)
import Time exposing (Posix)

clockProcedures : Block Memory Event
clockProcedures tid =
    [ Procedure.await <|
        \event _ ->
            case event of
                ReceiveTick time ->
                    [ Procedure.modify <|
                        \memory ->
                            { memory | time = time }
                    ]

                _ ->
                    []
    , Procedure.jump clockProcedures
    ]

You can use block or async for a similar purpose, but whereas they create new threads for the given Block; it causes threads overgrowing.

Safe pruning

Sometimes you may want to handle errors as follows:

unsafePruning : Block Memory Event
unsafePruning tid =
    [ requestPosts
    , Procedure.await <|
        \event _ ->
            case event of
                ReceivePosts (Err error) ->
                    [ handleError error tid
                        |> Procedure.batch
                    ]

                ReceivePosts (Ok posts) ->
                    [ Procedure.modify <|
                        \memory ->
                            { memory | posts = posts }
                    ]

                _ ->
                    []
    , Procedure.block blockForNewPosts
    ]

It appears to be nice, but it does not work as intended. Actually, the above Block can evaluate the blockForNewPosts even after evaluating handleError. To avoid this, you can use jump:

safePruning : Block Memory Event
safePruning tid =
    [ requestPosts
    , Procedure.await <|
        \event _ ->
            case event of
                ReceivePosts (Err error) ->
                    [ Procedure.jump <| handleError error
                    ]

                ReceivePosts (Ok posts) ->
                    [ Procedure.modify <|
                        \memory ->
                            { memory | posts = posts }
                    ]

                _ ->
                    []
    , Procedure.block blockForNewPosts
    ]

doUntil : Block memory event -> (event -> memory -> List (Procedure memory event)) -> Procedure memory event

Evaluate another Block, provided as a first argument, with new ThreadId until the second argument returns non-empty list.

For example, you could use it to define a function that executes the Block for appropreate SPA page until the URL changes:

import Thread.Procedure as Procedure exposing (Block)
import Url exposing (Url)

pageController : Route -> Block Memory Event
pageController route tid =
    [ Procedure.doUntil
        -- The thread for the `pageProcedures` will be killed
        -- when the URL canges.
        (pageProcedures route)
      <|
        \event _ ->
            case event of
                UrlChanged url ->
                    [ Procedure.jump <| pageController (routeFromUrl url)
                    ]

                _ ->
                    []
    ]

addFinalizer : Block memory event -> Procedure memory event

For a thread running this Procedure, add a finalizer: Procedures to be evaluated when the thread is terminated, such as when the last Procedure for the thread has finished to be evaluated, or when the thread is interrupted by quit or race, or the parent thread ends by such reasons.

Since addFinally appends the finalizer, it is especially important to note that if you use addFinally in a thread that self-recurses with turn, the finalizer will be executed as many times as it self-recurses.

modifyAndThen : (memory -> ( memory, x )) -> (x -> Block memory event) -> Procedure memory event

Modify the shared memory atomically, creating the intermediate value in the process, and pass the value to the another Block in the original thread.

The intermediate value is supposed to be the information of the certain resource at a particular time.

Conditions

when : (memory -> Basics.Bool) -> List (Procedure memory event) -> Procedure memory event

Evaluate given Procedures only if the first argument returns True with current memory state, otherwise returns none.

unless : (memory -> Basics.Bool) -> List (Procedure memory event) -> Procedure memory event

Evaluate given Procedures only if the first argument is False with current memory state, otherwise returns none.

withMemory : (memory -> Block memory event) -> Procedure memory event

Select a Block to run by the current memory state.

Do not use the provided memory state in the Block in order to avoid using outdated memory state.

Converters

These items are needed when you try to build a hierarchy of memory and events in an SPA.

These items are used to build memory and event hierarchies, for example in SPAs. Note that the pattern often unnecessarily increases complexity, so you should first consider using monolithic shared memory and events.

For a sample, see sample/src/Advanced.elm and sample/src/SPA.elm.

lift : Thread.Lifter.Lifter a b -> Procedure b event -> Procedure a event

Lift the memory type of of the given Procedure.

Note that this function does not set up a dedicated memory for b, but simply makes it operate on the part of memory a; so the memory b is shared with other threads. If you want to create a thread that allocates a dedicated memory area of type b for a given procedure, use functions in the Thread.LocalMemory module.

wrap : Thread.Wrapper.Wrapper a b -> Procedure memory b -> Procedure memory a

Wrap the event type of the given Procedure.

liftBlock : Thread.Lifter.Lifter a b -> Block b event -> ThreadId -> List (Procedure a event)

Block version of liftMemory.

wrapBlock : Thread.Wrapper.Wrapper a b -> Block memory b -> ThreadId -> List (Procedure memory a)

Wrap the event type of the given Block.

Lower level functions

It is recommended to use Thread.Browser for normal use.

init : memory -> Block memory event -> ( Model memory event, Platform.Cmd.Cmd (Msg event) )

initThreadId : ThreadId

ThreadId for the initially loaded procedure.

update : Msg event -> Model memory event -> ( Model memory event, Platform.Cmd.Cmd (Msg event) )


type Model memory event

extractMemory : Model memory event -> memory


type Msg event

mapMsg : (a -> b) -> Msg a -> Msg b

setTarget : ThreadId -> event -> Msg event

Set the target thread for an event by its ThreadId.