Elm Native basics
Before diving on the topic, don’t forget that
try/catch is your best friend if you have any doubt.
A simple and useless example
_[username]$[project name]$[module name].
project name are both coming from the
repository field inside your
elm-package.json, most of the time it will be your GitHub username and the name of your project. If you don’t have this field, default values
project will be used. So don’t forget to edit them to match your project.
Then we create an IIFE (aka a function that automatically called itself). That’s because we want to scope all our variables and functions. The returned value is an object exposing the API of our native module. Right now, we only have one function exposed but we could have way more.
Finally, because all functions in Elm are curried, we need to apply a small Elm helper to achieve that. You have several of them, from
F9, where the number match the number of arguments of your function. Since
add has 2 arguments, we wrap it inside
F2. There is no need to wrap functions with zero or one argument. We could also have directly wrote a curried function just like that:
F2 helper is easier.
Now, let’s save our code inside the
Native/Utils.js file. That’s because we named our module
Native part is mandatory but you can rename the
Utils part as you want. Just be sure to also change the variable name at the beginning of the native code. We can now import it inside our Elm code and use it.
This is a 200% danger zone. The Elm compiler is mostly like “Oh, you are using native? Ok, I will fully trust you on what you are doing but don’t complain if the world ends when your program crashes.” It means that the compiler will not perform any checks on types on any native function. For example, we could write:
And it will work just fine. The compiler will not try to do any type inference on native code. It’s totally up to you to make sure it will actually produce the expected result at runtime.
Protip This is why you should always add an Elm signature to your native functions by writing a corresponding Elm function which just call it directly. Just like we did, feel free to create several Elm functions with different signatures if your native function can handle them just fine. But never write code mixing standard Elm code with a native call in the middle of it, it will be a pain to understand its signature, it will be hard to debug and it will be easier to break at runtime.
That’s it! You can now use your
Utils module inside your Elm project.
Let’s go crazy
Obviously, you can write code that bends the limitations of Elm. All Elm functions are pure and without any side-effect. Among other things, it means that given the same arguments, a function will always return the same result. That’s why
Math.random is a
Task, because it isn’t pure. Using
Native, you can make it synchronous. We will see the example just below but never do that in your project, I just want to show you that it is possible.
Tasks are a huge part of any Elm program so it’s very likely that at some point, you will need to create them inside Native code. This is super useful for wrapping async Node functions. We will use other Elm helpers to achieve that. The main one is
_elm_lang$core$Native_Scheduler which is responsible for creating and finishing, either with a success or a failure, any tasks inside Native code.
First, we will call the
nativeBinding method of the scheduler. This will actually create an Elm task. But we need to give it a function as its first and only argument. This function will take a callback that you should call when your task is finished. This is how you can handle asynchronous tasks. You must wrap the result inside either the
fail methods of the scheduler.
Remember that Tasks are not only for asynchronous code. Any non-pure function, like
Date.now, should also be a Task.
Did you notice that
now is not a function? You don’t have to expose only functions in the returned API from a Native module, you can put whatever you want in it as long as you correctly use it inside your Elm code. Here, we directly create the
Task which is a wrapper around our actual code. It will be called using
Task.attempt later inside our program.