Use key, get_key and modify to reduce boilerplate in update

(0) I want to reduce boilerplate code in the Elm files I write. I’m happy to put the boilerplate elsewhere, particularly if it’s in a file that can be automatically generated from the rest of the project.

I’m looking for references to prior art, alternative approaches, and also feedback. I’d be delighted if someone else has already discovered and implemented this idea. That would mean less work for me!

(1) My toy problem is to create a selector for RGB values. So I start my app with

type Msg = R | B | G
init = { red = 0, blue = 0, green = 0 }

You can see the whole implementation on Ellie https://ellie-app.com/cDdvYdnJXkca1.

(2) Here’s my update method.

update msg state =
    let
        key = get_key msg
        prev = key state
        next = prev + 1
    in
    modify state key next

There’s no case in update. That’s hidden in get_key. We use modify to create the new state.

(3) All this can be made to work with Elm 0.19. Here’s some crucial parts of the implementation.

get_key msg =
    case msg of
        R -> .red
        B -> .blue
        G -> .green

lookup =
    { red = \record value -> { record | red = value }
    , blue = \record value -> { record | blue = value }
    , green = \record value -> { record | green = value }
    }

modify record key value =
    key lookup record value

(4) Much of (3) can be generated automatically. In particular, lookup needs only a list of all record keys in the app. And perhaps the get_key function for Msg can be similarly generated.

Hi @jfine2358

I find the approach interesting. That said, I don’t think it’s a particularly practical solution. (So this is more of a feedback comment, I don’t have much to bring on prior art or alternative approaches)

First of all, you mention you want to remove boilerplate. But the solution is not particularly terser.
I rewrote your program the way I’d do it here: https://ellie-app.com/cDfb48KRyKma1
The total amounts to 49 LOC (I removed comments and formatted using elm-format).
In comparison, yours (with the same changes) is at 58 LOC without counting the functions you showed in (3), 81 with (https://ellie-app.com/cDfmMKHdkCCa1).

Your solution does have the benefit of making it very hard to mix up colors ({ state | red = state.green + 1 } for instance). Also, if you have 100 colors, you program might be a bit terser because of the way that elm-format does formatting (no line-break in records).

Side-note: if you do have hundreds of colors, then you’ll also store an object (two actually) with 100 functions in memory. For simple things like these, that might be okay, but for very complex things you’ll need more data.

My second and bigger issue my worries is about how you’d extend the solution. Let’s say you want to add a reset button that resets all buttons. My feeling is that you’d have to either go through a lot of hoops in order to make your program still work in order not to change the update function, or do a case statement inside your update function where in the reset case you’d reset all values to 0, and in the other you’d do what you currently do (basically wrapping your current approach).

I think your approach works, but only so because you have such a special and uniform program. What if you had the same program, but for R you’d also have a reset button, for G you’d have a different text and for B you’d decrement the value? You’d probably do so by adding new records or new fields go your “configuration” records. In practice multiplying the data you’d have in memory (as I mentioned before).

In my more conventional approach, it would be reasonably trivial to handle all those cases.

I find that boilerplate brings flexibility, and it’s not something to necessarily fight against. Obviously, when some logic is very uniform, it might be interesting to add abstractions like those. But beware of premature abstraction and making your program harder to iterate on.

5 Likes

(0) Thank you @jfmengels for your interest and thoughtful comments. I’m grateful for your providing a more traditional solution, and doing a fair-minded comparison. You’ve raised many important issues. In this response I’ll focus on the size penalty of my approach. First I consider LOC. Then I consider asset size and memory use.

Later this week I’ll respond to your potentially fatal concern

My second and bigger issue my worries is about how you’d extend the solution

(5) (Here, all LOC counts are after elm-format.) My version has 58 LOC (excluding what I call the boilerplate). Your traditional solution has 49 LOC.

But don’t put too much weight on that comparison. To reduce LOC, I’ve replaced

update msg state =
    let
        key = get_key msg
        prev = key state
        next = prev + 1
    in
    modify state key next

by

update msg state =
    modify state (get_key msg) (get_key msg state + 1)

This reduces my excluding boilerplate LOC to 48. See https://ellie-app.com/cDhkX6QMZFwa1.

(6) You wrote:

The final size penalty depends on the optimisations the Elm compiler provides. According to Small Assets without the Headache, Elm 0.19 does function-level dead code elimination. It also provides record field renaming.

The Haskell wiki states

In general, splitting code across modules should not make programs less efficient. GHC does quite aggressive cross-module inlining: when you import a function f from another module M, GHC consults the “interface file” M.hi to get f’s definition.

(7) Assuming the Elm compiler does similar optimisations, I’d expect your solution and mine to produce similar values after suitable optimisation. But this expectation is based on ignorance, and not informed by experience.

You’re reinventing lenses :slight_smile:

1 Like

(0) Thank you for this @solificati. There’s some overlap, but I think I’m adding something that’s new and potentially useful.

Here’s a longish post to clarify. But in short, what I see that that both my idea and Moncocle.Lens use get and set functions. But I don’t see any other major similarities. I think that if you recode my RGB example using Monocle.Lens you’ll see the differences are important.

Joke: There’s some a nice piece of fruit, or an electronic gadget, waiting when you get to then end of this post.

(8) In Monocle.Lens I read:

type alias Lens a b =
    { get : a -> b
    , set : b -> a -> a
    }

So, not tested, the two statements below are equivalent and work.

red_lens = Lens .red  \record value -> { record | red = value }
red_lens = Lens .red  lookup.red

(9) The overlap is having get and set methods.

(10) The differences I see are

  1. Lens is considerable more general and abstract. My method uses only field access functions such as .red, .blue and more generally .name.

  2. The .name field access functions are already readily available.

  3. I suggest providing systematic ready access to the corresponding get functions.

  4. These get functions are already in wide use, but as literals rather than functions. The following are intended to be equivalent.

new_state = { prev_state | name = value }  -- Literal.
new_state = modify state .name value  -- Argument.
  1. In the above .name can be replaced by a parameter, such as key. This allows code branches to be combined. This can’t be done using the literal form.
new_state = modify state key value
  1. The system is to code the get functions for you. They’ll be guaranteed to be correct.

(11) In short, what I see is that both my idea and Lens use get and set functions. But I don’t see any other major similarities. I did read about Lens before writing my code. I didn’t understand it at all well, but I could see that it was unlikely to do what I wanted.

(infinity) By the way, here’s something that’s hardish to read. What color is this lens?

blue_lens = something

Related is fooling AI into thinking that an edible apple is an iPod electronic gadget.

I was thinking about more canonical definition of lenses from Haskell. You asked about prior art. In Haskell there is no nice record syntax so they also generate lenses via TemplateHaskell.

I assume Monocle is rather underdeveloped due to Elders of Elm not liking concept of lenses.

I totally agree that lines of code (or even number of characters) is not an important metric, especially when you have a compiler helping you prevent a lot of errors that can potentially happen and that the size of a program does not lead to more errors.

I guess it is important to wonder “what” boilerplate is though, from the point of view of someone who wants to reduce it. For some it is indeed about terser code, which makes sense because terser code can lead to easier to understand programs.

For me, the biggest potential problem with boilerplate is forgetting to connect elements (forgetting to call the update function or forgetting to call the subscriptions function) or misconnecting elements (doing { state | red = state.green + 1 } for instance).

In my experience, I find that the problem of misconnecting elements is very rare, even with large refactors. Forgetting to connect things is more common but I found the elm-review-unused to be quite good at detecting that (it won’t detect everything, but I know it has helped prevent people from shipping bugs to production already).

To me, boilerplate is simply connection logic, and while they feel like it could or should be omitted sometimes, it does add a lot of flexibility to how you can connect things.

Right. So for the record field renaming, it may indeed replace { red = ..., blue = ..., green = ... } by something shorter like { r = ..., b = ..., gz = ... }. So probably not a great gain, but something still.

As for function-level dead code elimination, it won’t apply too much in our cases. If for some reason, we never display the blue button, the Elm compiler is not smart enough to notice that the blue field can be removed everywhere (or even anywhere).

The difference in our solutions is that mine will potentially not evaluate some code until necessary (with the downside that those things will likely be re-evaluated every time it is needed) whereas with your solution those things will be evaluated at start-up time (slowing down the startup) and kept in memory (increasing memory usage), with the upside that it might be faster to re-use on every need.

These last differences will unlikely be noticed, but I think that the more you use this approach in an Elm app (up to using it everywhere) and the bigger your app is, the longer the initial startup time will be and the more memory the app will take. Though as always with performance: Benchmark before you try evaluate things and I will admit I have not benchmarked any of this.

My main concern with such an approach (and with almost all solutions that aim to reduce boilerplate) is how much you lose in flexibility, because the solution against boilerplate is using abstractions, and abstractions have a potential cost. Everything depends, so go for these solutions when you’re pretty darn sure what you are working with can be abstracted as you have done, and that it will likely not change, then cross your fingers.

This post suggests that cable (or wire) harness might be a better term that boilerplate.

According to wikipediate, Boilerplate text is

is any written text (copy) that can be reused in new contexts or applications without significant changes to the original. The term is used in reference to statements, contracts and computer code, and is used in the media to refer to hackneyed or unoriginal writing.

I don’t like the idea that a change to computer code isn’t significant. Perhaps there’s a better term that boilerplate. I suggest:

We are used to the idea of a test harness. According to wikipedia, a cable harness has terminals that allow it to be connected to the electrical components, and often a cable harness is subject to electrical tests after manufacture.

This is a sound concern, which influenced me to think of cable (or wire) harness as a better analogy than boilerplate. It has an element of skill and creativity associated with it, that the term boilerplate does not. Sometimes the connection logic is not simple, particularly if there are problems in getting the types consistent.

The cable harness analogy resonates strongly with me as I spent my first 4 years out of public school building them for aircraft. There were multiple times where we attempted to automate, in essence, portions of the process but it was rare that it was possible or practical to do so.

This in particular is so true. The most skilled of my coworkers at the time I always compared to artists because their final work was always one of a kind and immaculate.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.