arowM/elm-thread

Translate chronological specifications verbatim into applications.


License
MIT
Install
elm-package install arowM/elm-thread 2.0.1

Documentation

elm-thread

Build Status
Document
Live demo
Live demo (advanced)

logo

Extend TEA so that chronological specifications can be translated verbatim into applications.

What is this for?

With elm-thread, you can translate verbatim the specification of a UX-aware application into an implementation with the same look and feel.

In a UX-aware application, it is natural to write the specification in chronological order. This is because application users make decisions about what to do next, based on their experience of their previous operations and the application's response to those operations. However, conventional TEA is not suitable for implementing such specifications: Every time the user interacts with the screen, you have to check the model in the update function and try hard to analyze "what time series did the user follow" to choose the next process. A lot of bugs are introduced in this kind of transformation work. The bad news is that these bugs are about the behaviour of the application, so you have to suffer through complex and difficult UI testing.

With elm-thread, you can solve such drawbacks of TEA. As shown in the following example, it is possible to implement time series processing as it looks. What a magical library!

Terms

The terms referred to in this document are defined as follows:

  • Procedure: Definitions of the processes that the application will perform, in order.
  • Thread: Computational resources on which the Procedure is evaluated.
  • Local event: Events generated and received only within a specific thread.
  • Global event: Events generated by the UI, subscriptions, ports, etc. and received by all threads.

A Quick Example

The following code is an excerpt from sample/src/Main.elm.

import Thread.Browser as Browser exposing (Document, Program)
import Thread.Procedure as Procedure

main : Program () Shared Global Local
main =
    Browser.document
        { init = init
        , procedure = procedure
        , view = view
        , subscriptions = subscriptions
        }


-- Procedure


type alias Procedure =
    Procedure.Procedure Shared Global Local


procedure : () -> Procedure
procedure () =
    Procedure.batch
        [ sleep 3000

-- Hey, you know?
-- In the conventional TEA, every time you do a sleep operation,
-- you're sent to another branch of `update` function,
-- where you have to check your model to know "Where did I come from?".
-- What an annoying process!

-- With elm-thread, you just put the subsequent procedure right below it.

        , requestInitialTime

-- How intuitive to be able to write the result of the above request right underneath it!
-- Can I say one more amazing thing?
-- The result of the above request can only be received in this thread and has no effect on any other thread.

        , Procedure.await <|
            \local _ ->
                case local of
                    ReceiveInitialTime ( zone, time ) ->
                        Just <|
                            setPageView <|
                                PageHome
                                    { zone = zone
                                    , time = time
                                    , showActionButton = False
                                    }

                    _ ->
                        Nothing
        , putLog "Forking thread for clock..."

-- You can, of course, start and run another procedure as a thread independent of this one.

        , Procedure.fork <| \_ -> clockProcedure

-- The above procedure is running as an independent thread,
-- so the following procedures will run in parallel without waiting for them to finish.

        , modifyPageHome <| \home -> { home | showActionButton = True }
        , putLog """Press "Action" button bellow."""
        , Procedure.awaitGlobal <|
            \global _ ->
                case global of
                    ClickActionButton ->
                        Just <|
                            Procedure.batch
                                [ modifyPageHome <| \home -> { home | showActionButton = False }
                                , putLog """"Action" button has pressed."""
                                ]

                    _ ->
                        Nothing

-- Sometimes you want to synchronise your processes, don't you?
-- Use `syncAll` to make sure that all procedures are completed before moving on to the subsequent procedures.

        , Procedure.syncAll
            [ sleepProcedure1
            , sleepProcedure2
            ]

        , putLog "All child threads have completed."
        , Procedure.quit
        , putLog "(Unreachable)"
        ]


clockProcedure : Procedure
clockProcedure =
    Procedure.batch
        [ Procedure.awaitGlobal <|
            \global _ ->
                case global of
                    ReceiveTick time ->
                        Just <|
                            modifyPageHome <|
                                \home ->
                                    { home | time = time }

                    _ ->
                        Nothing
        , Procedure.fork <| \_ -> clockProcedure
        ]


sleepProcedure1 : Procedure
sleepProcedure1 =
    Procedure.batch
        [ putLog "Sleep 5 sec."
        , sleep 5000
        , putLog "Slept 5 sec."
        ]


sleepProcedure2 : Procedure
sleepProcedure2 =
    Procedure.batch
        [ putLog "Sleep 10 sec."
        , sleep 10000
        , putLog "Slept 10 sec."
        ]



-- Core


{-| The memory state shared by all threads.
-}
type alias Shared =
    { log : String
    , page : PageView
    }


init : Shared
init =
    { log = ""
    , page = PageLoading
    }


{-| Global events
-}
type Global
    = ReceiveTick Posix
    | ClickActionButton


{-| Local events that only affect a specific thread.
-}
type Local
    = ReceiveInitialTime ( Time.Zone, Posix )
    | WakeUp



-- View


type PageView
    = PageLoading
    | PageHome PageHome_


view : Shared -> Document Global
view shared =
    case shared.page of
        PageLoading ->
            pageLoading

        PageHome home ->
            pageHome shared.log home


pageLoading = Debug.todo "See `sample/src/Main.elm`"


type alias PageHome_ =
    { time : Posix
    , zone : Time.Zone
    , showActionButton : Bool
    }


pageHome = Debug.todo "See `sample/src/Main.elm`"


-- Subsctiption


subscriptions : Shared -> Sub Global
subscriptions _ =
    Time.every 1000 ReceiveTick

SPA Example

The following code is an excerpt from sample/src/SPA.elm.

import SPA.Page.Home as Home
import SPA.Page.Users as Users
import Thread.Lifter exposing (Lifter)
import Thread.Procedure as Procedure exposing (Procedure)
import Thread.Wrapper exposing (Wrapper)



-- Shared


type alias Shared =
    { home : Home.Shared
    , users : Users.Shared
    }


init : Shared
init =
    { home = Home.init
    , users = Users.init
    }


homeLifter : Lifter Shared Home.Shared
homeLifter =
    { get = .home
    , set = \home shared -> { shared | home = home }
    }


usersLifter : Lifter Shared Users.Shared
usersLifter =
    { get = .users
    , set = \users shared -> { shared | users = users }
    }



-- Global


type Global
    = GlobalEvent1
    | HomeGlobal Home.Global
    | UsersGlobal Users.Global


unwrapHomeGlobal : Global -> Maybe Home.Global
unwrapHomeGlobal global =
    case global of
        HomeGlobal home ->
            Just home

        _ ->
            Nothing


unwrapUsersGlobal : Global -> Maybe Users.Global
unwrapUsersGlobal global =
    case global of
        UsersGlobal users ->
            Just users

        _ ->
            Nothing



-- Local


type Local
    = LocalEvent1
    | HomeLocal Home.Local
    | UsersLocal Users.Local


unwrapHomeLocal : Local -> Maybe Home.Local
unwrapHomeLocal local =
    case local of
        HomeLocal home ->
            Just home

        _ ->
            Nothing


unwrapUsersLocal : Local -> Maybe Users.Local
unwrapUsersLocal local =
    case local of
        UsersLocal users ->
            Just users

        _ ->
            Nothing



-- Procedure


procedure : Procedure Shared Global Local
procedure =
    Procedure.batch
        [ Procedure.fork <|
            \() ->
                Home.procedure
                    |> Procedure.liftShared homeLifter
                    |> Procedure.wrapGlobal unwrapHomeGlobal
                    |> Procedure.wrapLocal
                        { wrap = HomeLocal
                        , unwrap = unwrapHomeLocal
                        }
        , Procedure.fork <|
            \() ->
                Users.procedure
                    |> Procedure.liftShared usersLifter
                    |> Procedure.wrapGlobal unwrapUsersGlobal
                    |> Procedure.wrapLocal
                        { wrap = UsersLocal
                        , unwrap = unwrapUsersLocal
                        }
        , Debug.todo "subsequent procedures..."
        ]