
Extend TEA so that chronological specifications can be translated verbatim into applications.
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!
The terms referred to in this document are defined as follows:
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 ReceiveTickThe 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..."
]