With* Functions in Elm
With* functions are functions that take the shape of
withProperty : Prop -> CustomType -> CustomType. When several are chained together using the pipeline operator they tend to produce terse, yet legible, code and a pleasant API.
Let’s consider some code that is a good candidate to refactor to use this pattern. Here is a module that exposes a configurable button.
This example shows several permutations of a button by leveraging an API that is expecting the following configurable record to be passed in.
Here we see a handful of optional properties which leads to somewhat verbose code at the call site. Examples like this are a great opportunity to use
At a high level, any property that is optional for a button will be considered for turning into a
with* function. Prior to that step let’s start with the required properties. What are the required properties of a button? Based on the above example, this is the list that I came up with.
label : String
onClick : Msg
That’s it! Did you come up with the same list? What about
size? Surely a button must know what size to render at. Or whether it should render as disabled or not.
While both of those statements are true for rendering a button, when I mention required property what I’m really getting at are required properties that do not have reasonable defaults. All of the other properties can either be left out entirely or they have reasonable default values.
Cool. Let’s get to refactoring and start off by defining a custom type and a
default constructor function.
Now that we have a way of creating a default button (with reasonable defaults!) let’s add some
with* functions to allow for configurability.
Admittedly, some of those function names read slightly awkwardly. The goal of this pattern isn’t necessarily to be dogmatic about using “with” as a prefix. Rather, this pattern is more about producing a pleasant API and separating required properties that don’t have safe defaults from other properties.
Below is an alternate API that might be more pleasant to use and read.
Is the above better? Maybe 🤷♂️! Discuss it with your team and find out what will work best for you!
Finally, we show all button permutations while leveraging the function pipeline operator and our new
make functions for a more pleasant API.
And everything all put together in Ellie!
Summing up some benefits
This approach works particularly well in situations when using a module that has a lot of optionality. In the above example we explored this approach with a UI element that intrinsically had many optional properties but this can also be true of other things like web requests, especially GraphQL.
Other benefits include:
- Encouraged use of hiding implementation details behind an opaque type
- As a result of the above point, versioning UI elements has fewer backwards incompatible changes. Imagine adding an optional
iconin both examples. When using the above pattern all exposed function type annotations stay the same with the exception that the module would also expose a new
- The code tends to be just as readable, if not more readable
- The code tends to be more terse especially at call sites that do not need to heavily override default values
- This approach can be very convenient with unit testing. If you’re not heavily writing fuzz tests this is a nice way to quickly mock up different inputs for passing to functions
- A related approach to
withfunctions with GraphQL queries (Dillon Kearns) - https://package.elm-lang.org/packages/dillonkearns/elm-graphql/latest/
- Robot Buttons from Mars (Brian Hicks) https://www.youtube.com/watch?v=PDyWP-0H4Zo