Skip to content

Instantly share code, notes, and snippets.

@cscalfani
Last active April 6, 2020 20:57
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cscalfani/7b2c4391aee0e3f69c677459870986ed to your computer and use it in GitHub Desktop.
Save cscalfani/7b2c4391aee0e3f69c677459870986ed to your computer and use it in GitHub Desktop.
Elm in Node (0.17)

Elm in Node (0.17)

Why?

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.

How?

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.

What does it do?

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.

Details

Node code

// 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.

Elm code

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.

@Pilatch
Copy link

Pilatch commented Nov 12, 2016

Awesome. Easiest proof of concept I found. Thanks! 😄

@jligeza
Copy link

jligeza commented Nov 1, 2017

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment