Calling Elm Functions from Node.js Code

(Update 2017-02-26: This is actually covered by the Elm docs, just in a more obscure place than I expected.)

If you just want to call Elm from JavaScript, see the GitHub repository. If you want the full saga of how I learned this, read on…

At RubyConf AU, I heard about Elm, a functional programming language based on JavaScript.

Now, I recently started building a set of command-line tools that are pure functions (i.e. they don’t keep running or change things, just process and return their input). These are currently in Ruby, but I’d like to use them for a browser app eventually, so I need something I can call from JS.

Pure function? Callable from JavaScript?

Seems like a job for a JS-based functional language!

1. Elm is for Browsers

The first thing I found out was that Elm is a language designed for web apps, to the point of having a built-in architecture and framework for them. So a lot of the features of Elm — and most of the examples — weren’t any use to me.

But I can still use it from the command line, right?

Google found me a project that has actually done this (Elm Oracle), along with a library extracted for the job, elm-node. But these looked complicated, so I decide to try something simpler first.

2. Simple, Obvious, and Wrong

I’ve already got a skeleton Node app, which just parses its command line arguments, loads a given file, and does nothing with it.

So can I just define an Elm function, compile it into JS, and call into it from the existing JS app?

test-func.elm

module Test exposing (..)

test : String -> String
test string = string ++ "a"

aeldardin.js

var requirejs = require('requirejs');

requirejs.config({
    nodeRequire: require
});

requirejs([
    'test-func'
  ],
function (testFunc) {

  debugger;
});
These files are from specific commits in the GitHub repository. Follow along there to see exactly what I did at each step.

Yes, I’m using RequireJS with Node. Remember, I want this to work in browsers eventually.

Setting up a project and testing that:

$ npm init
... answer various prompts ...
$ npm install requirejs

$ elm-make test-func.elm --output test-func.js

$ node debug aeldardin.js

... skip ahead (with 'c') to the right breakpoint ...

debug> repl
Press Ctrl + C to leave debug repl
> testFunc
{ Test: {} }
> testFunc.Test
{}

Where’s my function?

3. Ports on a Foreign Shore

Okay. So I can’t just define random functions; I’ve got to tell Elm I want functions the JavaScript can access.

At this point, I start reading through the Elm/JavaScript interoperability guide, which suggests I need to use “ports”.

With a few of my own adjustments because the spell-checker example wants to call out from Elm to JS, and I want my JS to call in, I end up with this:

test-func.elm (second revision)

port module Test exposing (..)

-- Elm module to get a sense of how to call Elm from JS
-- Very roughly based on https://gist.github.com/evancz/e69723b23958e69b63d5b5502b0edf90

import String

init : ( Model, Cmd Msg )
init = ( Model "", Cmd.none )

-- MODEL

type alias Model = { text : String }

-- UPDATE

type Msg
  = Done
  | Test (String)

port done : String -> Cmd msg

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Test newText ->
      ( Model newText, Cmd.none )

    Done ->
      ( model, done (model.text ++ "a") )

-- SUBSCRIPTIONS

port test : (String -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions model =
  test Test

However, now my Node program won’t even run:

Error: Evaluating /home/edward/Documents/projects/elm-nodejs-requirjs/test-func.js as module "test-func" failed with error: ReferenceError: _elm_lang$core$Json_Decode$string is not defined

This ticket suggests that I need to import Json.Decode:

changes to test-func.elm

 -- Very roughly based on https://gist.github.com/evancz/e69723b23958e69b63d5b5502b0edf90

+import Json.Decode
 import String

Now what does the debugger say?

debug> repl
> testFunc
{ Test: {} }
> testFunc.Test
{}

Why can’t I see those ports?

4. (Not So) Well-Known Names

After some diving through the generated test-func.js, I find this:

test-func.js (produced by elm-make)

var Elm = {};
Elm['Test'] = Elm['Test'] || {};
if (typeof _user$project$Test$main !== 'undefined') {
    _user$project$Test$main(Elm['Test'], 'Test', undefined);
}

if (typeof define === "function" && define['amd'])
{
  define([], function() { return Elm; });
  return;
}

It looks like _user$project$Test$main is how Elm encodes its dot-separated function names when compiled to JS. So it’s expecting me to define a Main function, which of course I’m not doing — which means none of my initialization code is getting called, and Elm['Test'] (the object I’m looking at in the debugger) is empty.

The second `if` block detects RequireJS, which will become important later.

After re-reading the elm-node example, and a bit more trial and error, I come up with this:

changes to test-func.elm

-port module Test exposing (..)
+port module Test exposing (main)

 -- Elm module to get a sense of how to call Elm from JS
 -- Very roughly based on https://gist.github.com/evancz/e69723b23958e69b63d5b5502b0edf90
-
-import Json.Decode
-import String
-
-init : ( Model, Cmd Msg )
-init = ( Model "", Cmd.none )
+-- and https://github.com/ElmCast/elm-node/blob/master/example/Example.elm
+
+import Platform
+import Platform.Cmd as Cmd
+import Platform.Sub as Sub
+import Task
+import Console
+import NodeProcess
+
+main : Program Never Model Msg
+main =
+    Platform.program
+        { init =
+          ( Model "", 
+            Cmd.none
+          )
+        , update = update
+        , subscriptions = subscriptions
+        }

That doesn’t compile:

$ elm-make test-func.elm --output test-func.js 
I cannot find module 'Console'.

Module 'Test' is trying to import it.

Potential problems could be:
  * Misspelled the module name
  * Need to add a source directory or new dependency to elm-package.json

5. When is a Package Not a Package?

Elm comes with a package manager, so this should be easy:

$ elm-package install elmcast/elm-node
Error: Could not find any packages named elmcast/elm-node.

Here are some packages that have similar names:

    cacay/elm-void
    Bogdanp/elm-route
    Bogdanp/elm-time
    debois/elm-dom

Maybe you want one of those?

No, I don’t want any of those.

After some digging, I find package.elm-lang.org, and discover there is no elm-node package. How do I actually use elm-node, then?

Looking closer at elm-oracle, I see this in the elm-package.json:

Elm Oracle’s elm-package.json, lines 6-9

    "source-directories": [
        ".",
        "elm-node/src"
    ],

Apparently the elm-node/src directory is a Git submodule. Well, I’ve never used that particular Git feature before, but it turns out it’s not too hard (at least for this use case):

$ git submodule add https://github.com/ElmCast/elm-node
Cloning into 'elm-node'...
remote: Counting objects: 174, done.
Receiving objects: 100% (174/174), 42.81 KiB | 0 bytes/s, done.
remote: Total 174 (delta 0), reused 0 (delta 0), pack-reused 174
Resolving deltas: 100% (91/91), done.
Checking connectivity... done.

Now I’m missing a slightly different module:

$ elm-make test-func.elm --output test-func.js 
I cannot find module 'Native.Console'.

Module 'Console' is trying to import it.

Potential problems could be:
  * Misspelled the module name
  * Need to add a source directory or new dependency to elm-package.json

This is because I need to explicitly enable native modules. Again, elm-oracle provides an example:

Elm Oracle’s elm-package.json, line 11

    "native-modules": true,

Published native modules?

I did wonder if you’d need to do this whenever you used a published Elm package with native modules (e.g. elm-d3).

But I’m not really installing elm-node. I’m just telling elm-make it’s part of my source code, which is why I need to turn native modules on. I imagine a properly published package wouldn’t need this.

That said, this situation does make me hope Elm has some support for loading packages from unofficial sources.

So, things are compiling again now:

$ elm-make test-func.elm --output test-func.js 
Success! Compiled 3 modules.                                        
Successfully generated test-func.js

But they don’t run. Not only that, the well-written error message is completely off-base:

Error: Evaluating /home/edward/Documents/projects/elm-nodejs-requirjs/test-func.js as module "test-func" failed with error: Error: You are trying to run a node Elm program in the browser!

6. Silly Rabbit, AMD is for Browsers!

I’m definitely not running this code in a browser, so why does Elm think I am?

Well, remember that AMD-detecting code we saw earlier?
It’s one of several framework-detecting blocks:

test-func.js (produced by elm-make)

if (typeof define === "function" && define['amd'])
{
  define([], function() { return Elm; });
  return;
}

if (typeof module === "object")
{
  module['exports'] = Elm;
  return;
}

var globalElm = this['Elm'];
if (typeof globalElm === "undefined")
{
  this['Elm'] = Elm;
  return;
}

The Native.Console module, however, has only this block to detect being run in a browser:

elm-node’s src/Native/Console.js, lines 51-53

    if (typeof module == 'undefined') {
        throw new Error('You are trying to run a node Elm program in the browser!');
    }

Since I’m using AMD and not Node’s own loading, module isn’t defined, and this check fails.

I’m getting a bit sick of this by now, so I just hack in a check for RequireJS’s define here too:

    if (typeof module == 'undefined' && typeof define === 'undefined') {
        throw new Error('You are trying to run a node Elm program in the browser!');
    }

That gives me a new error:

Error: Evaluating /home/edward/Documents/projects/elm-nodejs-requirjs/test-func.js as module "test-func" failed with error: ReferenceError: _user$project$Native_Process is not defined

7. Non-Native Speakers Need Not Apply

It seems the Native.Process module defines _elmcast$elm_node$Native_Process, and Elm expects it to be prefixed with _user$ instead.

Reading through Writing your first Elm Native module, and looking at the example cited (Elm-D3 again), it looks like Elm-node is using a legacy approach to native modules. Apparently now they’re supposed to define a make() function, rather than using these apparently internal-to-Elm names directly.

So it’s starting to sound like I need to dive into the details of this “undocumented on purpose” API just to call my little Elm function. Is it really this hard to call a simple function in a JavaScript-compatible language from my own JS code?

At this point, I decided I’d had enough, and turned off my computer for the night.

8. Epiphany

Lying in bed thinking about this, a thought hit me:

It’s only the Console and Process modules from elm-node that have this problem. Can I just not load that code?

changes to test-func.elm

 import Task
-import Console
-import NodeProcess
+

 main : Program Never Model Msg

Well, it compiles, but…

$ elm-make test-func.elm --output test-func.js && node aeldardin.js 
Success! Compiled 0 modules.                                        
Successfully generated test-func.js

/home/edward/Documents/projects/elm-nodejs-requirjs/node_modules/requirejs/bin/r.js:393
        throw err;
        ^

Error: Evaluating /home/edward/Documents/projects/elm-nodejs-requirjs/test-func.js as module "test-func" failed with error: ReferenceError: _elm_lang$core$Json_Decode$string is not defined

Hey, I’ve seen that error message before! Fix it again, and the code runs.

Now, what can I see in the debugger?

> testFunc
{ Test: { worker: [Function: val] } }
> var worker = testFunc.Test.worker()
> worker
{ ports: 
   { done: { subscribe: [Function: val], unsubscribe: [Function: val] },
     test: { send: [Function: val] } } }

That looks like a function I can call!

9. Doing Nothing Very Well

Let’s write some code to call into Elm:

aeldardin.js, now doing real work

// aeldardin.js (abridged)

var requirejs = require('requirejs');

requirejs.config({
    nodeRequire: require
});

requirejs([
    'test-func'
  ],
function (testFunc) {

    var worker = testFunc.Test.worker();

    worker.ports.done.subscribe(function(value) {
      console.log('From Elm ' + value);
    });

    var message = 'Hi';
    console.log('Before sending message: ' + message);
    worker.ports.test.send(message);
    console.log('After sending message: ' + message);
});

Well, it doesn’t crash — but it doesn’t seem to get any response from Elm either:

$ elm-make test-func.elm --output test-func.js && node aeldardin.js 
Success! Compiled 0 modules.                                        
Successfully generated test-func.js
Before sending message: Hi
After sending message: Hi

Stepping through things in the debugger suggests that Elm is never actually trying to call the JS itself, presumably because I’ve defined the ports incorrectly.

10. Finishing What I Start

When I look at the Elm code again, the problem is fairly obvious (with my six hours of new-found Elm experience, anyway). I’m only sending output in response to the Done message, not the Test message that the JS is actually sending me.

That’s a fairly easy fix:

changes to test-func.elm

 update msg model =
   case msg of
     Test newText ->
-      ( Model newText, Cmd.none )
+      ( Model newText, done (model.text ++ "a") )

     Done ->
-      ( model, done (model.text ++ "a") )
+      ( model, Cmd.none )

 -- SUBSCRIPTIONS

Now I get a response, but it looks like the model isn’t actually getting updated.

But of course it wouldn’t be! Since model is immutable, calling done with model.text is naturally going to use the old text.

The new text ought to show up if I send a second message to the port — and it does:

$ elm-make test-func.elm --output test-func.js  && node  aeldardin.js 
Success! Compiled 0 modules.                                        
Successfully generated test-func.js
Before sending message: Hi!
After sending 1st message: Hi!
After sending 2nd message: Lo!
From Elm a
From Elm Hi!a

11. Cutting to the Core

So I’ve got everything working, but one last question remains: did I actually need elm-node for this?

Apparently not. I can remove it entirely, along with all the hacks to make it work, without changing the output of my code.

This may prevent me using console.log for debugging. But given that I want this code to work in a browser eventually, elm-node probably wasn’t the solution for me in the first place; I’ll find a better way to cross that bridge when I reach it.

12. Looking for Meaning

What can I conclude from all this?

Well, doing new things is hard. Elm’s not really set up to work with Node.js yet, and I insisted on pushing that frontier anyway.

However, my only real challenge was a lack of documentation. It turns out all you need is a couple of port definitions and a Main function, and Elm will give you an object you can instantiate and call methods on from JS. (See my final code.)

What can Elm do about this?

I’m not sure much needs doing. They’ve already got instructions for using Elm in an existing JS app, and Node support doesn’t seem to be a priority for the moment.

And for the few people who do want to call into Elm from Node scripts, this meandering log may be of some help. 🙂

Leave a Reply