Native/Kernel modules in Elm 0.19

Elm 0.19 made it purposefully difficult to use what was previously called ”Native” and is now called ”Kernel” code. Both of those things mean calling JavaScript straight from Elm without going through ports. Doing that is of course a Bad Idea™ but what if you still want to do it?

So what’s the difficulty. These lines from Package.hs in the compiler are the principal problem

isKernel :: Name -> Bool
isKernel (Name author _) =
  author == "elm" || author == "elm-explorations

The meaning here is that Kernel packages are to be used only by the Elm core team and only in packages. Not end applications and not anybody else.

Suppose you wanted to have some Kernel in your project. Not perhaps because you needed it, but because you wanted it. Or it made your life easier. What you really should do, what the true real solution is, is fork the compiler. This post is not about that solution.

Hacking Elm to accept user Kernel code

Only packages by blessed authors are ok. Fine, let’s make one then. As far as I can tell there is no officially supported way to install dependencies from anywhere other than package.elm-lang.org. Luckily those are cached at ~/.elm and we can drop code there and have the compiler find it.

The structure of ~/.elm is something like the following

.elm                                
└── 0.19.0
└── package
├── elm
│ ├── core
  │ │ └── 1.0.2
│ │ ├── elm.json
│ │   ├── [ ... other files ]
│ [ ... other packages ]
├── elm-explorations
│ [ ... packages ]
├── [ ... other authors ]
└── versions.dat

One would think that one can just drop a folder in ~/.elm/0.19.0/elm/my-package/ and be done with it. Sadly no, as the compiler checks first that versions.dat for the package name and version requested. That file is a cached representation of the list of all packages received from an API request to package.elm-lang.org.

versions.dat is in a custom binary format that is quite understandable just by looking at it. One would think that one could just add a custom package in the elm namespace there. Sadly no, as the compiler seems to rewrite the file.

What does work is choosing a package name and version that exists in package.elm-lang.org but is not used by a project that is being worked on. In my case I just took the first elm-explorations package from the list, and that happened to be ”benchmark”.

This is what my file structure looks like

elm-explorations
└── benchmark
└── 1.0.0
├── elm.json
└── src
├── Elm
│   └── Kernel
│   └── Foobar.js
└── Foobar.elm

Contents of the files

Foobar.elm:

module Foobar exposing (alert)
 
import Elm.Kernel.Foobar
 
 
alert : String -> String
alert msg =
    Elm.Kernel.Foobar.alert msg

Foobar.js

/*
*/
 
 
function _Foobar_alert(str) {
  alert(str);
  return str;
}

elm.json

{
  "type": "package",
  "name": "elm-explorations/benchmark",
  "summary": "",
  "license": "MIT",
  "version": "1.0.0",
  "elm-version": "0.19.0 <= v < 0.20.0",
  "exposed-modules": ["Foobar"],
  "source-directories": [
    "src"
  ],
  "elm-version": "0.19.0",
  "dependencies": {
    "elm/core": "1.0.0 <= v < 2.0.0"
  },
  "test-dependencies": {
  }
}

Now you can add

"elm-explorations/benchmark": "1.0.0"

To your application dependencies and you have again access to Native/Kernel code. Of course if your project uses elm-explorations/benchmark, choose some other package that you don’t need.

If you are unsure about whether you need Native/Kernel code, just don’t. This is not a good idea. If you decide you do need/want Kernel in your project, consider gathering others like you and forking the compiler. If you have carefully considered the above two options but still want to just do it the hacky way: well, this is one way to do it.