Friday, June 3, 2016

Using Modules in Elm 0.17

Elm 0.17 introduces significant breaking changes from the previous release.  FRP has gone, as have signals, but what has improved is a more coherent architecture using the key new concept of subscriptions to external services.  These are managed for you by the elm runtime and will return Cmd messages to you in exactly the same way as (for example) a view component will return a message.  The overall architecture is nicely summarised in this picture.

There is no doubt that this is a major improvement, but unfortunately elm is still severely lacking in one crucial area - integration with the web platform API.  Evan has indicated how it is likely that this might be achieved in the future with a small set of examples which include web sockets and which rely on a still undocumented feature named Effect Managers.

The unfortunate result of this is that, if you want (say) to use a platform API such as web-audio, you are still encouraged to do so using ports, which provide a subscription to javascript services. These have major drawbacks - you are not allowed to build a library if you use them and are thus debarred from publishing it as a community package. Nor can you build a simple single artefact for distribution by other means - the javascript produced by your module must be hand-assembled alongside that produced by the calling program.

So, a good many developers will be forced down the road of using ports to access the platform API and producing their own modules as  'pseudo-libraries' for getting the job done.

Modules

Whereas there is good documentation for showing you how to build modules there is less available on the subject of how to incorporate a module into your program, particularly if it uses ports.  The rest of this article gives an explanation of how this might be done.

Suppose your module encapsulates a widget that plays back midi recordings on a suitable instrument. It might look like this, with a start/pause and stop button and a capsule which shows the proportion of the tune that has been played:



The module will use web-audio to actually create the sounds and so will use ports to a javascript service.  It will export a module definition looking like this:

  
    module Midi.Player exposing (Model, Msg (SetRecording), init, update, view, subscriptions)

This is all as expected. The only subtlety is that the module exposes the SetRecording message type that allows the calling program to tell it which recording to play. The messages that respond to the player buttons are hidden and act autonomously.

Main Program

The following section describes how the calling program that (somehow) gets hold of a MIDI recording via the MIDI message might integrate the player:

import

  
    import Midi.Player exposing (Model, Msg, init, update, view, subscriptions)

model

 
   type alias Model =
     { 
       myStuff :....
     , recording : Result String MidiRecording
     , player : Midi.Player.Model
     }

messages

The program needs to send a message to the player which describes the midi recording to play. Otherwise, all player messages must simply be delegated to the player itself:
  
    type Msg
      = MyMessage MyStuff
      | Midi (Result String MidiRecording )  
      | PlayerMsg Midi.Player.Msg   

initialisation

It is important that the calling program allows the player to be initialised. The let expression gets hold of the player initialisation command and then the Cmd map function turns the module-level message into a program-level message. The exclamation mark function requires some explanation - it is used here as a shorthand to convert the model into the (model, Cmd Msg) tuple.
  
    init : (Model, Cmd Msg)
    init =
      let
        myStuff = ....
          (player, playerCmd) = Midi.Player.init recording
      in
        { 
          myStuff = myStuff 
        , recording = Err "not started"
        , player = player
        } ! [Cmd.map PlayerMsg playerCmd]

update

It is assumed that the program issues a message somehow to get hold of a MIDI recording which it then saves to the model with an incoming Midi message once it receives the response. Thereafter, all module-level messages are simply delegated to the module:
  
    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model =
      case msg of

        MyMessage stuff -> ...

        Midi result -> 
          ( { model | recording = result }, establishRecording result )    

        PlayerMsg playerMsg -> 
          let 
            (newPlayer, cmd) = Midi.Player.update playerMsg model.player
          in 
            { model | player = newPlayer } ! [Cmd.map PlayerMsg cmd]
where establishRecording sends a command to the player which establishes the recording to play:
  
    establishRecording : Result String MidiRecording -> Cmd Msg
    establishRecording r =
      Task.perform (\_ -> NoOp) 
                   (\_ -> PlayerMsg (Midi.Player.SetRecording r)) 
                   (Task.succeed (\_ -> ()))

view

To see the player widget, you have to map the message onto the player view:
  
    view : Model -> Html Msg
    view model =
      div [] 
        [  
        myView ..
        ,  Html.map PlayerMsg (Midi.Player.view model.player) 
        ]

subscriptions

Similarly, you must map the subscriptions onto those of the MIDI player (alongside any subscriptions the program requires for other purposes):
  
    subscriptions : Model -> Sub Msg
    subscriptions model = 
      Sub.batch 
        [  mySubs ...
        ,  Sub.map PlayerMsg (Midi.Player.subscriptions model.player)
        ]

Final Integration

The complete source of the MIDI player module can be found here. An example of a final html file that integrates the javascript from the player module with that of a calling program named MidiFilePlayer can be found here.