Elm

Fri Jan 7 '22

Elm is a functional programming language. It compiles to JavaScript and has a bunch of libraries (like elm/browser) for building software to run in a web browser.

It has some generics. There’s an option type, Maybe, and a Result type.

This, along with the static type checking in the compiler, allows type expressions and compile-time guarantees that plain JavaScript does not offer.

It’s super fun. And I’m going to say some nice things about it and then explain why I won’t use it in the future.

The tooling is nice.

The compiler (written in Haskell) is quite fast compared to my experiences with a lot of JavaScript tooling or even the TypeScript compiler.

The output for compiler errors is user-friendly too. Here’s a type with three variants and a function to return a CSS class corresponding to a given variant.

type Outcome
    = Whatever
    | Good
    | Bad

outcomeClass : Outcome -> String
outcomeClass outcome =
    case outcome of
        Good ->
            "yay"

        Bad ->
            "nay"

And this is what the compiler says about this code where a variant is not handled by the case statement.

Detected problems in 1 module.
-- MISSING PATTERNS ----------------------------------------------- src/Roll.elm

This `case` does not have branches for all possibilities:

291|>    case outcome of
292|>        Good ->
293|>            "yay"
294|>
295|>        Bad ->
296|>            "nay"

Missing possibilities include:

    Whatever

I would have to crash if I saw one of those. Add branches for them!

Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.1/missing-patterns> for more
guidance on this workflow.

And, if that’s not enough kool-aid, there’s tool called elm-format that formats your source code. Even making nice whitespace adjustments and removing unneeded parenthesis. For example:

thing = (foo bar) baz

… becomes …

thing =
    foo bar baz

It’s great! We’re living in the future.

And I really like the way it looks.

-- from https://elm-lang.org/examples/buttons

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

I’m a fan of indentation and whitespace being significant in language syntax. Not everyone will agree; but, {} brackets is punctuation to scan through and indentation already serves the purpose.

But, being new to functional programming, it took some time for me to figure out how to read type signatures and function composition in Elm without a bunch of punctuation everywhere.

For example, a type in Rust (or C++ templates I think?) might resemble List<Foo> or Result<Foo, Error>. But something similar in Elm might be List Foo or Result Error Foo.

Similarly, function calls read without commas delimiting parameters because functions only really take one argument. If you want to do something with two parameters, like make a new string by joining two input strings, you might have a a function that takes the first string and returns a function that takes the second string and then returns a new string by joining the two inputs.1

It might look like:

wow : String -> String -> String
wow a b =
    a ++ " " ++ b

… and could be invoked with …

wow "hello" "world"

… which is the same as …

(wow "hello") "world"

… because the wow "hello" expression evaluates to a function and everything is just partial applications and order of operations.

The order of operations thing is a big deal because you can use operators with different precedence to avoid parenthesis everywhere.

Method Chaining

The |> operator is great. It kinda lets you do method chaining without methods.

Look at this Rust code (taken from docs for an HTTP library called hyper) that uses method chaining to initialize an HTTP client.

let client = Client::builder()
    .pool_idle_timeout(Duration::from_secs(30))
    .http2_only(true)
    .build_http();

You don’t care about how it works2, only that it looks cool.

This is also a popular thing with the Iterator trait in Rust.

let s = ["alpha", "beta", "gamma"]
    .iter()
    .map(|s| s.chars())
    .flatten()
    .collect::<String>();
assert_eq!(s, "alphabetagamma");

However, this syntax requires methods, not just any function you have lying around.

If you want to want to put something into the chain that isn’t a method you have to make it a method. In Rust, this is possible by creating a trait but it is mildly awkward.

In Elm, there are no methods, but you can do some cool pipelining. Here are three expressions that do the same thing.

"13, meow, 5, 7, -20"
    |> String.split ","
    |> List.filterMap (String.trim >> String.toInt)
    |> List.filter ((<) 0)
    |> List.sum
    |> String.fromInt
    |> text

String.fromInt
    (List.sum
        (List.filter
            (\n -> 0 < n)
            (List.filterMap
                (\s -> String.toInt (String.trim s))
                (String.split "," "13, meow, 5, 7, -20")
            )
        )
    )

String.fromInt
    << List.sum
    << List.filter (0 |> (<))
    << List.filterMap (String.toInt << String.trim)
    << String.split ","
<|
    "13, meow, 5, 7, -20"

You can read it forwards, or backwards, or like a pyramid, or mix and match all of these.

There’s nothing special about the functions involved – you don’t have to write a trait to introduce something into the chain. It’s just partial function application.

You don’t need parenthesis or nesting everywhere and the statements compose into like a pipeline or something.

I think it looks great and makes me happy when I read it.

Bonus Meme

Also, method chaining irritates Robert Cecil Martin who writes:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

This kind of code is often called a train wreck because it look like a bunch of coupled train cars. Chains of calls like this are generally considered to be sloppy style and should be avoided. It is usually best to split them up as follows:

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
Clean Code by Robert Cecil Martin

The more I read this, the better it gets.

“This … is often called a train wreck”. Who does this?

“… it look like a bunch of coupled train cars.” Okay, lets say it looks like train cars. How do you get from “coupled train cars” to a “train wreck”?

Now, Robert probably didn’t spend much time on this part of the book – and I hate to be an anti-fan – but the suggestion is a little funny to me.

We introduce two variables in our scope, opts and scratchDir. Now, if I were to have scratchDir and outputDir in scope I would seriously consider making them the same type or using dissimilar names. Personally, I can see myself having a hard time remembering if scratchDir is a file handle or a path like outputDir is. And having memorable names is important.

I mean, in this case, it’s kind of silly because opts and scratchDir aren’t used later on – so their names don’t have to be meaningful – but, that fact may not be apparent to someone reading this for the first time.

So I don’t know what Robert is thinking here. At this point, I’m pretty sure he just really doesn’t like trains.

I made a thing.

A bit ago I used Elm to make a webshit for rolling dice. You can check out the project on sr.ht or I might still be hosting a copy of the webshit at dice.froghat.ca.

Later on, I tried to use Elm for another project.

The problem I had with Elm.

The author of Elm, Evan Czaplicki, gave a talk at some point somewhere (elm-conf 2016?) where he explained that the way he does project management is different from how it normally is done.

Normally, he explains, projects aim for zero open issues. So, as issues comes in, they’re addressed individually. One at a time, each issue corresponds to some resolution like a change in the project.

But, for Evan’s projects, issues come in and are left to marinate and to be pondered over by the wise such that they can be considered holistically. In this way, connections between multiple issues can be found and then addressed at once and by comparatively fewer changes overall.

Moreover, by ignoring some issues outright, releases are simpler because they don’t change as much. And, by releasing infrequently, there is less churn and projects that depend on ours don’t have to spend as much time upgrading.

Evan emphasizes that his way is a weird and quirky way of doing things. But I don’t think anybody really does the first thing.

People have lives and, when a new issue comes in for a project they work on, it’s quite normal for something else to be more immediately important. Even when you aren’t bouncing between projects, and it’s your full time job, and you have help, a backlog is a totally normal thing just because doing stuff takes more time than complaining about it.

And, I don’t know that it makes sense to treat bugs and features in the same way here. Sometimes, a bug has a pretty clear and obvious solution. You can make one change for it that doesn’t break any APIs and it doesn’t need to marinate.

Also, when I hear Evan describing this process, it sounds like he’s speaking on the time scale of weeks. Maybe months for slower projects or for complicated features.

Last year, I stumbled into and reported a bug with the elm/virtual-dom package. But I don’t think Evan has been active on that package since 2018. Maybe my issue, and the others that have been reported to Elm and left open over the years, are marinating. Wow, what a wacky and unique process.

Why don’t you fix it?

Elm does not have a traditional Foreign Function Interface with JavaScript.

Even though Elm compiles to JavaScript, you can’t just invoke any arbitrary JavaScript from Elm.

Some packages, like elm/virtual-dom, depend on specific JavaScript APIs, like Document.createElement, in order to function. These packages include some special JavaScript code to translate between the two languages. But the compiler will not build packages with the JavaScript sources required for the translation unless they fall under the “elm” or “elm-explorations” namespaces.3 That’s just how it’s designed.

As a result of this special treatment, users cannot fork elm/virtual-dom and publish their own version in their own namespace.

You can hack around this by messing with the compiler or with how packages are resolved on your system,4 but that limits your ability to share your work because others then need the same hacks.

I won’t going to argue with the motivations stated in the document linked above on guide.elm-lang.org. But I think it’s fair compare Elm to Go a little bit. Even though Go doesn’t compile to another language like Elm does, they both have a runtime that makes interoperability a little bit more complicated. And, I think the points in that document can, to some degree, be argued for both Elm and Go.

Nevertheless, Go has its Cgo thing that lets Go code call into C.

Now, my Go lore is spotty, but it seems as if Evan’s concerns about package flooding and safety haven’t shown up in Go’s ecosystem. Instead, the community demonstrates a preference for “pure Go” implementations and bears attitudes that correspond to the kind of thing that Evan seems to have been trying to reach. But Go didn’t neuter their toolchain to get there.

To be clear, it is Evan’s right to do whatever he wants with Elm. But I think that Go’s approach has been more successful at providing utility and even at avoiding the pitfalls that Evan was trying to avoid when he made FFI a privilege denied to Elm’s users.

In the end, the lack of an FFI5 in Elm prevented me from doing what I needed to finish a project. I switched the project over to Rust targeting WebAssembly and was able to release it.

Compared to a lot of other options, Elm has pleasant tooling, an okayer community, and great documentation. And I think it’s a very fun language. But, the fact that Elm’s community packages don’t get to play by the same rules as Evan’s, demonstrates his lack of belief in the creative potential of his community and comes at the cost of missed opportunities for Elm itself.