marc walter

An Elm REPL for my phone

2024-09-28 (Last updated on 2024-10-31)

I have been building elm.run/repl to write, type-check and compile Elm code on my mobile phone even if I don't have internet access.

Behavior

As visible in the preview on the left, there is an input area at the bottom where I can enter multiple lines of Elm code. The input field grows as needed, and I tried my best to ensure that it is usually not hidden by the on-screen keyboard on my phone.

With every keystroke, the code is checked and if it is valid I will see the type definition above my input (in the example the declaration elm = "delightful") and the evaluated result below, which is the String "delightful".

If I press the "run code" button, it will print the entire compiler output, which is useful for finding errors like in the example where a number was supplied insted of the expected String to create a Tree type alias.
The formatting is supplied by the compiler and looks like it would in the console.

I can scroll up to see older code that I entered, can easily copy each down into the input area with one button press or remove it completely.
It also has a button to execute multiple bulk clean-up operations. From a full reset to removing all outdated declarations to only removing errors, I have multiple options there.

Apart from colors to make the errors and the input field more readable, I tried to keep the amount of distractions low.
It has a light and dark mode, and auto-detects the user's preferences.

For people just starting out, I added an introductory text at the top of the page and a button to insert example code.
Right now it randomly picks one of several examples each with multiple entered expressions.

read more

Generating short unique string identifiers from numbers with sqids-elm

2024-08-06 (Last updated on 2024-09-01)

I ported the sqids library to Elm, which generates short unique string IDs from numbers.
This is good for link shortening, fast & URL-safe ID generation and also storing relations between multiple numbers.

Interesting parts of the implementation

It has a shuffle algorithm that does not need random and always produces the same output given the same input.
It is designed for imperative languages though, requiring many swaps in an array.

I exposed two constructors: One that expects a configuration record, and one that follows the builder pattern.
Working that out was fun.

Sqids.Context.from
    { alphabet = Sqids.Context.defaultAlphabet
    , minLength = 0
    , blockList = Sqids.Context.defaultBlockList
    }

Sqids.Context.new
    |> Sqids.Context.withAlphabet "abc123"
    |> Sqids.Context.withMinLength 1
    |> Sqids.Context.withBlockList [ "block", "disallowed" ]
    |> Sqids.Context.build

Releasing a package was also a nice experience

To aid with creating a new Elm package, Dillon Kearns created two awesome resources: The elm-package-starter template and the idiomatic Elm package guide.

After looking at those, two more things were great to have:

  1. elm-doc-preview is an awesome tool to preview the documentation. It is the main reason why I did not not create patch versions 1.0.1 to 1.0.10 (or even more) because I would not have been satisfied with the visuals on the package site. It is especially useful to look at the generated order of the document on the page and for the code snippets as part of the documentation.
  1. And lue-birds elm-review rule to verify the code examples both on the README and inside the documentation was great! It saved me a lot of copy-paste and wrong documentation.
read more

Code bundle splitting of Elm programs

2024-03-02

Splitting the code bundle is not a topic that comes up often with Elm, because most times we are blessed with small bundles without the headache.
These improvements introduced in 2018 remove unused Elm code before the JavaScript code is generated. So no useless JS code makes it into the bundle when using the --optimize flag and dead code is effectively eliminated.

To summarize the first two links: Elm js bundles are small in comparison.
For instance the Elm implementation of TodoMVC weighs 9kb (if uglified), and the Elm realworld example weighs 29kb. For comparison the React runtime alone weighs 32kb (back in 2018).
From the list of maintained realworld examples, I looked at a few react versions and this one seems to be one of the smallest ones (in bundle size), and generates two files totaling 85kb (2.a9e8cf08.chunk.js 77kb and main.7edcaa81.chunk.js 9kb).

All numbers assume that the JS code was minified (e.g. with uglify-js or terser) and gzipped.

There is also a comparison from 2020 looking at bundle size, lighthouse performance score and time-to-interactive, where Elm performs very well.

Nevertheless, if I'm now assuming that my Elm program bundle is too big and I want to reduce the initial bundle size, I can split my single big Elm program into multiple smaller ones.

To skip ahead, in the end we can load each program from one js file and all common code (like the Elm runtime) is placed in a shared file that is imported by each program and downloaded only once.

read more

Testing the fixes that are distributed with elm-janitor

2023-11-25

Because several core Elm packages have known bugs with proposed bugfixes, the elm-janitor github org forked the packages and applied some fix PRs.

To test that these patches actually work, I created a test suite repository with:

  1. Tests that only need an Elm test runner
  2. Tests that need a fake browser environment
  3. Tests that need an actual browser

The repository is also useful to develop new patches for the upstream core Elm packages.

First time using deno

2023-08-12

For this script I used deno for the first time, and I liked the experience:

1. The permission system

I like the permission system, I can give very exact permissions what to access. I don't have to worry that a dependency might wipe my disk if I only expect it to left-pad a string.

ELM_HOME=$PWD/elm-home deno run --allow-env=ELM_HOME,HOME --allow-read=$PWD --allow-write=$PWD/elm-home --allow-net=github.com,codeload.github.com,api.github.com  https://raw.githubusercontent.com/elm-janitor/apply-patches/main/deno/cli.ts --all

This example code can only read and write to the file system where I want it to, access two environment variables, and contact a few domains.

If I don't specify permissions, or github switches the domain to serve code, the runtime asks me. That way a long-running script does not have to start from the very beginning.

┌ ⚠  Deno requests net access to "codeload.github.com".
├ Requested by `fetch()` API.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

2. No transpiling of TypeScript needed

3. Publishing to npm

It is not hard to publish a tool written in deno to npm thanks to the dnt (Deno to Node transform) package tool.

Using patched Elm core packages vetted by elm-janitor

2023-05-23 (Last updated on 2023-11-25)

Several core Elm packages have known bugs with proposed bugfixes as pull requests, which were not yet merged.
These fixes are merged in batches by Evan, so it could be that they will only be merged with the next official Elm release.
That might be a way off because Evan wants to finish his exploratory work for Elm on the backend.

Rupert and other Elm community members have started grouping and ranking PR fixes, and created an online spreadsheet.

There is also a series of posts on the Elm discourse where the process is explained.

Right now, there is a github organization elm-janitor that contains forks of the official Elm repositories with several bugfixes merged in.

From https://github.com/elm-janitor/manifesto#elm-core-library-maintenance

elm-janitor is a group of Elm users that are interested in maintaining the Elm core packages by applying patches to them. The patch stack will always be applied on top of the latest official release of the Elm core packages, with patches removed from the stack if they become part of the official release.

To avoid manually downloading patches and placing theem in the correct places, I created a script, elm-janitor/apply-patches

read more

Optimizing bundle size of Elm programs

2021-09-09 (Last updated on 2022-10-18)

Elm is well known for generating small bundle sizes (screenshot), as explained on the official website.

But after seeing this post about minifying Elm code I also wanted to investigate if the same results hold true when using the compiled Elm bundle as an ES module instead of the default IIFE output.

I want to use ESM, because I also load other parts of my applications in that format and I want to avaoid a global Elm state.

I have reached pretty much the same conclusion as Simon Lydell already published on the Elm discourse

  • For the smallest file size using a single tool, you can either pick UglifyJS or Google's closure compiler. Both take about the same time (9.5-10s in my case).
  • But you can achieve the same size result by running just parts of UglifyJS and then esbuild in about 40% of the time (~3.5s).
  • Esbuild alone produces about 10% more code than UglifyJS, but it needs only ~2% of the time (0.1s) when directly writing to a file.
  • SWC is comparable to Esbuild in compilation time, but not yet in compression ratio (16% bigger). They are working on reaching parity to terser, so re-visiting that in the future will be interesting.
  • Terser produces ~4% more code than UglifyJS but only needs 60% of the time.

Full results are documented on this page.

Debugging in Elm

2021-02-12

Programming in Elm is pretty awesome, but sometimes you still have to do some debugging.
Like most languages, the language tooling can help you.

Debug.log

Elm has the Debug.log function that allows you to print any Elm data structure to the browser console. This works well for small structures, but it only prints it as one line and that is hard to read if it contains more data.
Fortunately, the Elm community member kraklin has created an Elm debug helper browser extension for that, so your code printed with Debug.log will become an expandable tree and might look like this:

Example output

Image source: Nicer Debug.log console output by @kraklin

Even if you can't or don't want to install it as an extension, you can still use it directly as an npm package or by adding a minimal snippet to your HTML in any browser.

<script src="https://unpkg.com/elm-debug-transformer@latest/dist/elm-console-debug.js"></script>
<script>ElmConsoleDebug.register()</script>

Time-Traveling Debugger (in elm/browser)

debugger button Debugging in Elm is quite nice with the time-traveling debugger.

You don't need to install a browser extension like the react developer tools or something like that, you just need to compile the code with the debug flag, e.g. elm make --debug src/Main.elm.

Then you will see the debugger in the bottom right corner of your page, can see the internal state and all the commands that were executed, and can go forward and backward in time to reproduce for instance a bug.

read more

Installing all Elm dependencies on a CI system

2020-12-20

When configuring a CI pipeline for an Elm project, we ran into a couple of issues: The first problem was that the build step (which executes elm make) sometimes failed because downloading the Elm packages failed.
And the second one was that we were not able to easily cache the dependencies between runs in ~/.elm.

A failing pipeline is definitely annoying, and even more so if it looks like invalid code was pushed, if in fact only downloading dependencies failed because the package server is not reachable.

How we solved it:

First we changed the folder where all dependencies are downloaded to using the ELM_HOME environment variable. This allows the CI system to persist the cache easily, without needing access to ~/.elm. So the dependencies only need to get downloaded once and are then cached.

Then we faked a command to install all Elm dependencies. For that we created a minimal Elm file, compiled it (which downloads missing dependencies even though they are not used), and then removed it again.

As a simple script:

export ELM_HOME=elm-stuff/home

# create an Elm source file that definitely will compile
printf 'module InstallElmDeps exposing (main)\nimport Html exposing (text)\n\nmain = text "install"\n' > src/InstallElmDeps.elm
# run the compiler without output
elm make src/InstallElmDeps.elm --output /dev/null
# remove the file
rm src/InstallElmDeps.elm