Some [rules] can be bent. Others can be broken. Understand? -- Morpheus
This projects provides pure Elm tools whose aim is to make programming in Elm more composable even if it means bending a bit the rules of The Elm Architecture.
A classic Elm application that follows The Elm Architecture is structured like this:
- a
model
data type representing the model at any given time. - a
msg
data type representing events either fired by the view or the runtime (mouse click, input change, http response, ...) called messages. - the
update: msg -> model -> (model, Cmd msg)
function is called on every message received to compute the new value of the model. It can output commands which are tasks handled by the runtime. On completion, commands return a message which trigger once again theupdate
function. - the
view: model -> Html msg
renders the current value of the model into HTML content. This HTML can define messages to send on events.
This approach has some limitations:
- The callback passed to
onInput : (String -> msg) -> Attribute msg
and many other event handlers can decide which message to send based on the input string, but is forced to send one. - if you want to execute a command in response to an event happening in the view, the view has to trigger a message that will be interpreted by the update function which will output the command ...
- the command type (Cmd) is not a monad. It means commands do not compose! For example chaining commands has to be handled in the update function or by using another type such as tasks.
All of this makes perfect sense from an architectural point of view. The Elm Architecture has many benefits like isolation of rendering, state and effects. This project is for those ready to trade these benefits for more flexibility and conciseness. If your update function is littered with command scheduling code or/and your message type looks more like boilerplate than business, then this package is made for you!
You have two options:
- the CmdM approach lets you program the way you used to but lets you trigger commands in the view and chain commands as you like. The model is still updated in the update function, not in the view!
- the IO approach, in addition of the CmdM's benefits, lets you read and write the state directly in commands. You can then alter the state directly from the view.
The CmdM monad
The CmdM monad is the command type (Cmd) turned into a monad. It enables to chain effects easily using classic monadic operations without having to encode complex scheduling in the update function.
A program using CmdM is generally built arround CmdM.program, CmdM.vDomProgram, CmdM.programWithFlags or CmdM.vDomProgramWithFlags depending on if this is a headless program or if flags are required. For more specific needs, you can use CmdM.transform and CmdM.transformWithFlags.
CmdM is used very much like
Cmd. The main difference
is the view outputs Html (CmdM Msg)
instead of Html Msg
. You're not forced to refactor your view
to use CmdM:
classicTeaView: Model -> Html Msg
cmdmView: Model -> Html (CmdM Msg)
cmdmView model = classicTeaView |> Html.map CmdM.pure
The general way of using CmdM is
lifting a Cmd a
value into a CmdM a
one by CmdM.lift
and chain them by CmdM.andThen or CmdM.ap. The module CmdM.Infix provides infix notation for these operators.
The IO monad
The IO monad is like the CmdM monad enriched with state altering effect. Thus command effects and model modifications can be mixed easily. Furthermore the view and subscriptions can not only emit messages but also IOs.
Here is a complete example of a simple page showing a counter
module Hello exposing (..)
import Html exposing (..)
import Html.Events exposing(..)
import IO exposing (..)
type alias Model = Int
type alias Msg = ()
increment : IO Model Msg
increment = IO.modify ((+) 1)
reset : IO Model Msg
reset = IO.set 0
view : Model -> Html (IO Model Msg)
view m =
div [] [
h1 [] [text "Example of an IO program"],
p [] [text ("Counter = " ++ (String.fromInt m))],
button [onClick increment] [text "increment"],
button [onClick reset] [text "reset"]
]
main : IO.Program () Model Msg
main =
IO.sandbox {
init = \_ -> (0, IO.none),
view = view ,
subscriptions = IO.dummySub
}
Like CmdM, a program using IO is generally built arround one of the many IO.*Program* functions. These function cover web and headless programs, run with or without flags. In addition the functions named beginner* offer a simple and conside way to run most IO programs. For more specific needs, you can use IO.transform and IO.transformWithFlags.
With IO, reading and writing the model is done with IO.get, IO.set and IO.modify. It means this kind of code becomes possible:
action : IO Model Msg
action =
IO.get |> IO.andThen (\model -> -- First we read the model
let
-- The classic Http command
httpCommand : Cmd (Result Error Model)
httpCommand = Http.get { url = "https://example.com/my/api/action"
, expect = Http.expectJson identity decoder
}
in
-- First we lift the Cmd command into IO
-- then compose it by andThen with a function to deal with the response
IO.lift httpCommand |> IO.andThen (\response ->
case response of
Ok newModel -> IO.set newModel -- and set the new model on success
Err _ -> IO.none -- or do nothing on failure
))
Requiring all IO actions to work on the whole model would break composability, which would be petty bad obviously. Fortunately IO play well with optics:
import Monocle.Lens exposing (..)
-- An IO action whose model is an integer
actionOnInt : IO Int ()
actionOnInt = IO.modify (\x -> x + 1)
type alias Model = { number : Int, name : String }
lensFromIntToModel : Lens Model Int
lensFromIntToModel =
{ get = \model -> model.number,
set = \i model -> { model | number = i }
}
-- an IO action whose model is a Model
actionOnModel : IO Model ()
actionOnModel = IO.lens lensFromIntToModel actionOnInt
To avoid having to use optics when not needed, it is advised to use CmdM for model agnostic actions and lift CmdM to IO at the last moment by IO.liftM.
Examples from http://elm-lang.org/examples translated into CmdM and IO
The examples folder contains examples from http://elm-lang.org/examples converted into CmdM and IO ways. Please read the README.md file in this folder for more details on examples.
If you have questions and/or remarks, contact me on twitter at @chrilves