DEV Community

Cover image for A guide to Native code and Effect Managers in Elm - Part 1: Commands
Julian Antonielli
Julian Antonielli

Posted on • Updated on

A guide to Native code and Effect Managers in Elm - Part 1: Commands

This is a guide to understand how Elm implements its core libraries, and how you could write your own.
This article assumes experience writing Elm and JavaScript.

Elm uses a combination of the following resources to implement core libraries (those under the elm and elm-explorations organisations, like elm/core and elm-explorations/test):

  • Regular Elm code
  • Native/Kernel code
  • Effect Managers
  • Compiler magic

We won't discuss the last one here.
One thing to note is that some packages only use a few of those, for example elm/project-metadata-utils is implemented in pure Elm.

Note: Native code and Effect Managers can only be compiled by the elm and elm-explorations1 organisations, so these are out of reach of regular users.
If you still want to use these features, you can use a forked version of the compiler to do so, but I won't discuss that here.

Even if you are not interested in building your own native modules, I still think it's interesting to know how they work, as they are how The Elm Architecture is implemented under the hood.

Native/Kernel Code

We'll start with Native, or Kernel code. This is the terminology Elm uses for accessing JavaScript code from Elm.

The simplest example of this I could find is the Debug.log function from the elm/core package:

Debug.log : String -> a -> a

Calling Debug.log "My label" { a = 2 } will print "My label: { a = 2 }" to the console and will return the same value we passed in as second argument.
If we take a look at the source code, the implementation is just delegating to a function called Elm.Kernel.Debug.log:

-- src/Debug.elm
log : String -> a -> a
log =
  Elm.Kernel.Debug.log

If you look through the repository, you won't find a Elm/Kernel/Debug.elm module, you'll find a JavaScript file instead. In this file we'll find our sought function:

// src/Elm/Kernel/Debug.js
var _Debug_log__DEBUG = F2(function(tag, value)
{
    console.log(tag + ': ' + _Debug_toString(value));
    return value;
});

As expected, the function just logs our label and value (after using another javascript function to convert it to a string) and returns it.

Note: In the file above, you can see various functions with a suffix of __PROD and __DEBUG in their names. The Elm compiler picks which of those versions you use based on whether you compile your code with optimisations or not: it will use the __PROD version for optimised code, and the __DEBUG version for unoptimised/debug code.

Note: Kernel modules must be defined under the /Elm/Kernel/ directory hierarchy. From Elm, you can import them as if they were regular modules.
Functions in the native module have to follow the convention of being named like _MyKernelModule_myFunction.

module MyElmModule

-- Imports the kernel module:
-- src/Elm/Kernel/MyKernelModule.js
import Elm.Kernel.MyKernelModule


foo =
    Elm.Kernel.MyKernelModule.myFunction

Finally, you should give type annotations to kernel functions in Elm,
if you don't, the compiler will accept whatever type you give it, and you'd be susceptible to runtime exceptions from type errors.

This ends our introduction to Kernel code. If you're interested in some of the more nitty-gritty details, feel free to read the appendix.

Effect Managers

Effect Managers are a special type of Elm modules that are used to implement commands (like Random.generate) and subscriptions (like Browser.Events.onResize).
In this article, we'll implement a command from scratch, we'll discuss subscriptions in the next article, as they are a bit more complex.

We'll implement a command (Cmd, from now on) that, when dispatched (through a regular update function), will display a window.alert with a user-selected text.

Here's how an app using it would look like here:

module Main exposing (main)

-- Our effect manager
import Alert
import Browser
import Html exposing (Html, button, text)
import Html.Events exposing (onClick)


type Msg
    = SendAlert String


view _ =
    button
        [ onClick (SendAlert "Hi there!") ]
        [ text "Click to get an alert" ]


update msg model =
    case msg of
        SendAlert text ->
            ( model, Alert.alert text )


main =
    Browser.element
        { init = \_ -> ( {}, Cmd.none )
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

(Also available here)

On clicking the button, you would get an alert with the text "Hi there!".

Let's implement the Alert effect manager.

The anatomy of an Effect Manager

Instead of the usual module MyModule exposing (myFunction) module declaration, an effect manager starts like this:

effect module Alert where { command = MyCmd } exposing (alert)

Effect managers (which define commands) are required to implement a type (here MyCmd, but you can name it however you'd like), and the following functions:

type MyCmd msg

cmdMap : (a -> b) -> MyCmd a -> MyCmd b

init : Task Never state

onEffects : Platform.Router msg event -> List (MyCmd msg) -> state -> Task Never state

onSelfMsg : Platform.Router msg Never -> Never -> state -> Task Never state

Let's go through what each one does, by implementing our Alert example.
The MyCmd type describes which types of commands our module is capable of producing and performing.

We only offer one command, which only needs to dispatch an alert with a user-selected text, no other information is needed:

type MyCmd msg = Alert String

That was easy.

Now, let's go for cmdMap. This function gets used whenever a user calls Cmd.map on one of our commands. Given that we don't store a msg or anything related to it, our implementation is very simple:

cmdMap : (a -> b) -> MyCmd a -> MyCmd b
cmdMap _ (Alert text) = Alert text

Now we're getting into the more interesting details, we need to implement init, onEffects, and onSelfMessage.

To understand what each of these is for, we need to understand the following: Effect Managers are very similar to Elm applications, they have:

  • A Model, usually referred to as State in this context
  • An init function, which determines the initial State, with the only difference being that you're allowed to use a task (and thus, perform side-effects)
  • And finally, two update functions: onEffects and onSelfMessage, we'll discuss them in a minute

For our case, we don't really need to maintain any state, so we can just use an empty record {}, and our init will be very simple too.

type alias State =
    {}

init : Task Never State
init =
    Task.succeed {}

Finally, onEffects and onSelfMessage is where the magic starts to happen, we'll ignore for a second its first parameter, the Platform.Router.
Our onEffects will be called whenever an application dispatches one (or more, through things like Cmd.batch) of our commands, and we receive it in the second argument. Our third argument will receive the current State. In this function we're supposed to handle these commands, and update our state if we needed to:

-- src/Alert.elm
onEffects : Platform.Router msg event -> List (MyCmd msg) -> State -> Task Never State
onEffects router commands state =
    case commands of
        [] ->
            Task.succeed state

        (Alert text) :: rest ->
            let
                _ =
                    Elm.Kernel.Alert.alert text
            in
            onEffects router rest state

In words, if we don't have any commands to handle, return the current state (wrapped in a task, because the signature forces us to). Else, handle the first one (in this case we do it by calling a kernel function), and recurse with the rest of the commands.

In case you're wondering, our kernel code for this is unsurprisingly simple (__Utils_Tuple0 is just a way to return Elm's empty tuple, ()):

// src/Elm/Kernel/Alert.js
function _Alert_alert (text) {
  window.alert(text)

  return __Utils_Tuple0
}

For our example, that's basically it! Unfortunately, we do have to implement onSelfMsg anyway. Before this, let's understand what the first argument to it and onEffect is.

I did say Effect Managers are like regular Elm applications, and while our commands are sort of Msgs, we don't control when they're sent, users do. Turns out, it'd really useful to have self-controlled messages as well in these modules, and so we do!

From the type signature above, events are our msgs. The router allows us to send ourselves a message, through Platform.sendToSelf. These events will be routed to our onSelfMsg function, and we can use them to update our state. Also, some commands do return messages back to the application, and we can do that with Platform.sendToApp.

Going back to our example, given that we don't really need any self-sent messages, we can set our event type to Never and implement onSelfMsg in the only way possible:

type alias Event =
    Never

onSelfMsg : Platform.Router msg Event -> Event -> State -> Task Never State
onSelfMsg _ _ state =
    Task.succeed state

We've now implemented all the pieces we're required to by the compiler for an Effect Manager, but we're missing the reason why we started implementing our module at all, the alert command!

Our first attempt would be to do the following:

alert : String -> Cmd msg
alert text =
    Alert text

But this will give you a type error, Alert text is a MyCmd msg, not a Cmd msg!
Now, remember that strange where { command = MyCmd } part of our module declaration? That gives us a magical command function that allows us to convert our MyCmds into proper, application-viable Cmds.

alert : String -> Cmd msg
alert text =
    command (Alert text)

And we're finally done!
You can view (but not compile) the full code for this effect manager here.

Note: Here, we've used the names MyCmd, MySub, State, Event, and so on for the types we've defined, but you can actually pick whatever name you want for each of these, this is just the convention that's typically used in elm/core's effect managers.

Appendix

Nuances of interfacing with JavaScript

While this guide is meant to be a more of a resource on reading native code, I can give some pointers in case you're interested in writing some.

In regular Elm all functions are curried, and so, functions with multiple arguments really take one at a time and return a function for each one. This is not the case in JavaScript.

To overcome this mismatch, Elm uses a few helper functions on the JavaScript side, going back to our Debug.log example, in the JS source you'll see the following:

var _Debug_log__DEBUG = F2(function(tag, value)
{
    console.log(tag + ': ' + _Debug_toString(value));
    return value;
});

That F2 function is basically currying the 2-argument function we to passed it so that it can match the way Elm handles function calls. In the source, you'll find functions F2 through F9 for converting JavaScript functions with 2 to 9 arguments to a curried version.

Conversely, there are a series of functions called A2 through A9 which make calling Elm functions from JS easier:

That means that instead of:

// JavaScript
myElmFunction(a)(b)(c)(d)

We can use our A4 helper to invoke this function more naturally:

// JavaScript
A4(myElmFunction, a, b, c, d)

These functions also perform some optimising tricks to reduce the number of function calls/allocations. If you're interested in that, you can read exactly what they do in the source, here.

Bundling of Kernel code

Some Kernel (JavaScript) code will require using functions from other modules, for this, Elm uses the same import Module exposing (value) syntax, in the form of "magic" JS comments.

You can see an example of this in the Elm.Kernel.Char module of the elm/core package:

/*

import Elm.Kernel.Utils exposing (chr)

*/

In that same file, the chr function will be accessible as __Utils_chr.

Top comments (3)

Collapse
 
emlautarom1 profile image
Martín Emanuel

Really interesting! I'm pretty sure I'll need some of this info later so I better save this. Thanks for the explanation!

Collapse
 
sirseanofloxley profile image
Sean Allin Newell • Edited

Great deep dive into an effects manager! ✨

Collapse
 
csaltos profile image
Carlos Saltos

Great !! ... thank you for sharing !!