Sharing code between the client and the server in a Universal Javascript application is a big gain. No more are the days of rewriting code for the server.
But moving from Javascript to Elm in the front end can feel like a move backwards. So much of the code we write is environment independent. And it would be great if we could leverage some of the front end logic on the backend.
Elm's current focus is the front end so support for node is sketchy at best. But since Elm compiles to Javascript, we can take a peek at the code and figure out how to make it work in Node.
WARNING: There's no guarantees that this will work in the next release of Elm. However, I suspect that migration will be minor.
This simple program is a proof of concept. Javascript sends a String to Elm which prints it out in the console so we know it got there and the Elm program sends Time to Javascript which prints it out to the console.
Interop is important since Elm code on the server won't be able to leverage any existing Effects Managers since those have been written for browsers. Maybe this will change in future releases but for now all we have are ports.
Technically, it should be possible to write Effect Managers on the server using Elm Native code but that's beyond this article.
// compile Main.elm with:
// elm make Main.elm --output main.js
// load Elm module
const main = require('./main');
// start Elm runtime with NO renderer, hence worker, and then get Elm ports
const ports = main.Main.worker().ports;
// every second send Elm the string 'testing'
// first see subscription function in Elm
// where the message DisplayInput will be sent when data is received by Elm code
// then see DisplayInput message in the update function in Elm
setInterval(_ => ports.testIn.send('testing'), 1000);
// subscribe to the output of the Elm code
// see: Tick message in the update function in Elm
ports.testOut.subscribe(time => console.log('time from Elm:', time));
// this gets printed first
console.log('done');
The first thing this program does is load our Main.elm
module which has been previously compiled to main.js
.
Then it gains access the input ports via the worker
function which has no render.
Next a regular interval timer is setup to send the string testing
to the Elm module.
Then it subscribes to outputs from the Elm module which in this particular case is Time in Elm (a float in JS).
And finally, it prints done
which should print first since the logging of the timed communications won't happen for 1 second.
port module Main exposing (..)
import Html exposing (..)
import Html.App
import Time exposing (Time)
import Json.Encode
import Json.Decode
{-| input from JS
-}
port testIn : (String -> msg) -> Sub msg
{-| output to JS
-}
port testOut : Time -> Cmd msg
type alias Model =
{}
type Msg
= DisplayInput String
| Tick Time
main : Program Never
main =
-- N.B. the dummy init which returns an empty Model and no Cmd
-- N.B. the dummy view returns an empty HTML text node
-- this is just to make the compiler happy since the worker() call Javascript doesn't use a render
Html.App.program
{ init = ( Model, Cmd.none )
, view = (\_ -> text "")
, update = update
, subscriptions = subscriptions
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DisplayInput str ->
let
-- this is here just to output something so I know testIn works
-- N.B. Debug.log is NOT pure and is only here for TESTING
input =
Debug.log "input from JS" str
in
( model, Cmd.none )
Tick time ->
-- outputting to a port is a Cmd msg
( model, testOut time )
{-| subscribe to input from JS and the clock ticks every second
-}
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch
([ testIn DisplayInput
, Time.every 1000 Tick
]
)
The most notable part of the code is the init
and view
entries in the first parameter of Html.App.program
. Our module doesn't have a renderer, but we need subscriptions so we have to use Html.App.program
.
So they are dummy entries here just to appease the compiler. BTW, view
will never be called since we called worker
in the Node code.
Other than that, the Elm code is pretty standard.
It doesn't work for 0.18, but here is a working example: https://www.reddit.com/r/elm/comments/5eone2/elm_worker_in_018/