All Projects → chrilves → Elm Io

chrilves / Elm Io

Licence: mit
Pure IO like monad

Programming Languages

elm
856 projects

Elm-IO

Documentation

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 the update 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.

Example

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.

Need help?

If you have questions and/or remarks, contact me on twitter at @chrilves

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].