(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
Pure function? Callable from JavaScript?
Seems like a job for a
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
module Test exposing (..)
test : String -> String
test string = string ++ "a"
var requirejs = require('requirejs');
requirejs.config({
nodeRequire: require
});
requirejs([
'test-func'
],
function (testFunc) {
debugger;
});
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
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:
This ticket suggests that I need to import Json.Decode:
-- 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.
After re-reading the elm-node example, and a bit more trial and error, I come up with this:
-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:
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:
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:
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”
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?
import Task
-import Console
-import NodeProcess
+
main : Program Never Model Msg
Well, it compiles, but…
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:
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.
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
What can Elm do about this?
I’m not sure much needs doing. They’ve already got instructions for using Elm in an existing
And for the few people who do want to call into Elm from Node scripts, this meandering log may be of some help. 🙂
Hello Isikyus,
thank you for the writeup.
It was interesting to see how you approached this problem.
I spent some time today finding a way to directly call the functions emitted by the elm compiler. (I ended up taking a route without ports or subscriptions)