code generation with elm

generating elm code with elm code (and some js)


someone reached out to me recently about how i generated Elm code with Elm for elm-spa. At a high-level, this post is about the things you'll need to do to create your own library.

designing for the elm community?

Code generation is neat, but having a well-designed package is a better outcome! Elm folks can easily understand functions and data types over your custom library.

creating the elm project

Let's create a new elm project from the command line:

Those commands will create a new project in the codegen folder.

From there, we'll use Platform.worker to create a "headless" elm app that doesn't render to the DOM with a view function.

We can do this by creating src/Main.elm:

The way our Platform.worker will communicate with the outside world is with ports. These will allow us to send messages back and forth to NodeJS!

Let's create a src/Ports.elm file for that code

For now we'll only expose Ports.send, which when given a String, will send a message to NodeJS!

Let's update our src/Main.elm to send that message on init (when the app starts up):

That's enough Elm for now, let's make sure your app compiles:

Hopefully, you'll see Success! and you'll have a new file at dist/elm.js.

the nodejs part

Alright, let's use the Elm program we just compiled in a new NodeJS program at index.js:

This is a simple program that does three things:

  1. imports the elm app we compiled earlier
  2. starts the app
  3. listens on the send port for messages.

Run the app like this:

You should see this in the output:

code generation!

Alright, if all the steps above are working, we're ready to generate some Elm code!

Let's imagine this is the app we want to make:

  1. We receive a list of pages. ("Dashboard", "AboutUs", "NotFound")
  1. We need to generate a dist/Route.elm file like this:

Let's upgrade our existing app piece by piece to support this.

but first!

Something I did not do when writing the elm-spa code generator was use elm-test. That was stupid, I feel stupid. Now I come back to my code and am very spooked.

I'm not much of a unit testing kinda guy, but holy boy I should have written tests for these things because i forgot how they worked like a week later.

setting up elm-test

That's it– great job, we'll add some in soon!

taking input from nodejs

Let's add in some flags, so we can take in JSON input from NodeJS

Here's src/Main.elm

And here's the new index.js

Let's see if our Elm app received them, by printing them back out with our Ports.send from earlier:

Let's rebuild the app, and run it again!

You should see this output:

Sick, bro!

rendering an elm file

The next step is to use a different function than String.join ", " to convert the input into a dist/Routes.elm file.

Let's create a module called src/Route.elm to contain our functions:

And we'll use Routes.render in the init function in src/Main.elm:

Now when we recompile and run the app:

We should see this printed out in the console:

module Route exposing (routes)

import Url.Parser as Parser exposing (Parser)

type Route
    = Dashboard
    | AboutUs
    | NotFound

routes : Parser (Route -> a) a
routes =
        [ Dashboard (Parser.s "dashboard")
        , AboutUs (Parser.s "about-us")
        , NotFound (Parser.s "not-found")

"string interpolation" in Elm

To make the code printed out dynamic, we'll need to replace the hard-coded custom type and parsers with ones generated from the data.

To do that, we'll need to replace the content in the template with a string that reflects the code we want.

This is the technique I used for elm-spa

By using the {{variableName}} syntax, I can use String.replace to insert the dynamic value into the string where I want it.

As a convention, I made the variable name match the name inside {{}}, so it would be easier to find.

I also found it helpful to create an indent function to take care of the proper tab formatting for me.

unit tests!

Something I failed to do with elm-spa was to write unit tests for the Utils.Templates functions, and especially for the Route.render function.

This time around, I used elm-test to write tests like these:

Having simple examples of what I expect is super useful when referencing things again later. I really wish I had done this work up front when working on elm-spa...

It was even more useful for Route.render, where I could see the full result of a template.

As the template code got more complex, I found myself wishing for a simple example of input/output I could glance at to understand what I was looking at!

If you'd like to see the actual implementation for Utils.Template functions or things like the Route.toParser function, you can click those links.

They're just functions that return String values, so I didn't want to get into them too much here!

actually creating code

Alright, so if we update our index.js to send in different input data:

And run the latest code, we should see the console print out this:

module Route exposing (routes)

import Url.Parser as Parser exposing (Parser)

type Route
    = Dashboard
    | AboutUs
    | Careers
    | NotFound

routes : Parser (Route -> a) a
routes =
        [ Dashboard (Parser.s "dashboard")
        , AboutUs (Parser.s "about-us")
        , Careers (Parser.s "careers")
        , NotFound (Parser.s "not-found")

Awesome, our new Careers item is inserted where we'd expect!

Let's actually use NodeJS to write out that string as a file: dist/Route.elm, and we're done!

After importing fs and path at the top, and replace the console.log with something like fs.writeFileSync, we can actually write a file out to the file system.

If we build and run our app one last time:

This time, a new file is ready at dist/Route.elm, and it looks like this:


That's it– We made Elm code with Elm code (and like 20 lines of JS)! For your project, you might get your input list from another source.

For example, elm-spa uses the names of items in the src/Pages folder to determine what code to generate.

Thanks for reading, hope this post was useful!

Feel free to check out the project on Github!

next up:

elm canvas thing

August 4th, 2019