Record Type Alias Combinators: A Public Service Announcement
Tags: elm, combinators, designsystem, functional
Published: 2023-12-18 05:23:51

I've been trying and failing to write this essay for well over a week, so I'm going to start by unburying the lede and we'll take it from there:

I've figured out how to leverage Elm's type system in such a way so as to allow for the creation of record type aliases that create a constraint against the constructors that a given record type alias must expose. Sort of like... record type alias combinators.

These record type aliases can be composed to expose a set of chained, nested "builders" for a type; and they can be implemented for any type.

I'm doing my best here; perhaps a smarter and kinder soul than I, can explain my own discovery to me a little better, and if any of you can do that, please do - but here's what I'm getting at - you can generalize and combine _properties on record type aliases_.

----

Imagine that you're building a new design system. From scratch. You just finished reading Refactoring UI[^1], and you're 100% ready to "limit your choices" and "define systems in advance" and "pick five colors that you like but don't call them by their names, call them some semantic thing like `Primary` or `Danger` or whatever".

Color's easy. We'll start there. You know the drill: from the void, conjure a type; give it named constructors; map them to a value:

```
type Color
    = Primary
    | Secondary
    | Success
    | Danger
    | Warning
    | Info
    | Light
    | Dark


colorToAttr : Color -> Html.Attribute msg
colorToAttr color =
    Attr.style "background-color" <|
        case color of
            Primary ->
                "#0d6efd"

            Secondary ->
                "#6c757d"

            Success ->
                "#198754"

            Danger ->
                "#dc3545"

            Warning ->
                "#ffc107"

            Info ->
                "#0dcaf0"

            Light ->
                "#f8f9fa"

            Dark ->
                "#212529"
```

Hey, alright - now we can just use our `Color` type to create attrs. Easy.

Wait, hold on - incoming Slack message - Product wants to know:

> You're building this design system around light mode and dark mode, right?

Okay okay, nothing we can't handle:

```
type DisplayMode
    = LightMode
    | DarkMode


modedColorToAttr : DisplayMode -> Color -> Html.Attribute msg
modedColorToAttr displayMode color =
    case displayMode of
        LightMode ->
            colorToAttr color

        DarkMode ->
            Attr.style "background-color" <|
                case color of
                    Primary ->
                        "#0d6efd"

                    Secondary ->
                        "#6c757d"

                    Success ->
                        "#198754"

                    Danger ->
                        "#dc3545"

                    Warning ->
                        "#ffc107"

                    Info ->
                        "#0dcaf0"

                    Light ->
                        "#f8f9fa"

                    Dark ->
                        "#212529"
```

There we go. These colors will work for....... buttons.

Definitely not for backgrounds. Or for lighter variants. Sigh. Time to start building up a bunch of types. With parameters. Zipping around everywhere.

Drowning in a pile of types, again.

I mean, I _guess_ we could just do it in CSS... right? CSS sucks, but it's a known quantity of suck, and the designers know it, and... I guess we'll never have a fully-specified design system.

----

But what if there was another way? What if there was a way to create tighter constraints, that didn't require developers using a design system to pass around types?

What if there was a way, as the gardener-in-chief of a design system, to create generic _kinds_ of specifications, and compose them together?

What do I always say, on this website?

**We're better. And we can do better. _LFG._**

----

## Designing a System to Design Design Systems

Here's the trick - we have to start thinking in terms of creating generic specifications for these design system elements.

For instance - what if we could _generalize_ over the idea of a color palette - over the choices between `Primary`, `Secondary`, `Danger`, etc - and then _generalize_ over the choices between `Background` and `Font` and `Border` - and compose them together, and still have some type safety, and implement different things differently at each call site, while still leaning on the compiler?

You people have got to check this out. Let's build a type. You wanna build a type? Let's build a type, let's do it.[^2]

```
type Hue
    = Primary
    | Secondary
    | Danger
```

So far, nothing out of the ordinary, right? I mean, let's call it `Hue` instead of `Color` - `Color` is a bigger idea - let's save it for something bigger.

```
type Target
    = Background
    | Font


type Opacity
    = Op100
    | Op70
    | Op30


type Accent
    = Normal
    | Subtle


type Color
    = Color Target Opacity Accent Hue
```

"John, it seems like we're drowning under a pile of types again--"

Let me cook. I know you know how to make a type. This is all setup, and if you want to enable this kind of flexibility in your design system - you're going to have to specify it. And you're going to specify it eventually, regardless - but if you don't specify it on purpose, you'll specify it on accident. We have a word for that, but I can't remember what it's called.[^3]

```
type alias AllHues builder =
    { primary : builder
    , secondary : builder
    , danger : builder
    }
```

 > A type alias? You're repeating the names of the constructors as properties? And assigning them to a type parameter?

I am. And quickly now, I'm going to do it again for the others:

```
type alias AllTargets builder =
    { background : builder
    , font : builder
    }


type alias AllOpacities builder =
    { op100 : builder
    , op70 : builder
    , op30 : builder
    }


type alias AllAccents builder =
    { normal : builder
    , subtle : builder
    }
```

So what did we just do, there?

For every value in the constructor of the `Color` type, we specified the possible constructors as a record type alias with a type parameter.

And a type parameter can be anything. Even a function. But it truly can be _anything_ - what good is `AllHues` if the values for `primary`, `secondary`, and `danger` are open?

Let's make them concrete by providing an implementation - a function that, for each record property, supplies a value that matches that property:

```
allHues : (Hue -> builder) -> AllHues builder
allHues toBuilder =
    { primary = toBuilder Primary
    , secondary = toBuilder Secondary
    , danger = toBuilder Danger
    }


allTargets : (Target -> builder) -> AllTargets builder
allTargets toBuilder =
    { background = toBuilder Background
    , font = toBuilder Font
    }


allOpacities : (Opacity -> builder) -> AllOpacities builder
allOpacities toBuilder =
    { op100 = toBuilder Op100
    , op70 = toBuilder Op70
    , op30 = toBuilder Op30
    }


allAccents : (Accent -> builder) -> AllAccents builder
allAccents toBuilder =
    { normal = toBuilder Normal
    , subtle = toBuilder Subtle
    }
```

Do you see it, yet?

Try this: what happens if we just call one of these functions?

If we were going to do that, what kind of function would we pass in as the `(a -> builder)` parameter?

Let's try `identity` - can't hurt:

```
allHues_ =
    allHues identity
```

Let the compiler infer the type.

What type is it?

Oh, it's an `AllHues Hue`.

What happens if we access a property on that value?

```
someHue =
    allHues_.primary
```

It's... a `Hue`?

So, all of the props on `allHues_` are `Hue`s. That's not too surprising - but that was just with passing in `identity`.

We could pass in... other functions... couldn't we? And then each property on `allHues_` would be - a `Hue`, applied to _some function_.

Check this out - check out this type:

```
type alias AllColors builder =
    AllTargets (AllOpacities (AllAccents (AllHues builder)))
```

It's.......

- An `AllTargets builder`, where `builder` is
    - An `AllOpacities` builder, where `builder` is
        - An `AllAccents builder`, where `builder` is
            - An `AllHues builder`.

What if we made the final `builder` concrete?

```
type alias ColorBuilder =
    AllColors Color
```

And provided an implementation?

```
colorBuilder : ColorBuilder
colorBuilder =
    allTargets
        (\target ->
            allOpacities
                (\opacity ->
                    allAccents
                        (\accent ->
                            allHues
                                (\hue ->
                                    Color target opacity accent hue
                                )
                        )
                )
        )
```

What... what is this thing?


![GIF of `Color` type construction from record type alias combinators, with autocomplete from the LSP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7jh1pp1q6694aat3t1rk.gif)

It's a set of constraints with a concrete implementation that builds up successive, nested applications of values on a function.

Now that you have a single, unambiguous implementation of a `Color` type that describes all of the colors and their variations - you can build a renderer for that type; and _that renderer_ can be the single source-of-truth for all of the values that it supports:

```
colorToAttr : Color -> Html.Attribute msg
colorToAttr (Color target opacity accent hue) =
    let
        targetFragment : String
        targetFragment =
            case target of
                Background ->
                    "background-color"

                Font ->
                    "color"

        opacityFragment : String
        opacityFragment =
            case opacity of
                Op100 ->
                    String.fromFloat 1.0

                Op70 ->
                    String.fromFloat 0.7

                Op30 ->
                    String.fromFloat 0.3

        rgbFragment : String
        rgbFragment =
            case ( accent, hue ) of
                ( Normal, Primary ) ->
                    "13, 110, 253"

                ( Normal, Secondary ) ->
                    "108, 117, 125"

                ( Normal, Danger ) ->
                    "220, 53, 69"

                ( Subtle, Primary ) ->
                    "207, 226, 255"

                ( Subtle, Secondary ) ->
                    "226, 227, 229"

                ( Subtle, Danger ) ->
                    "241, 174, 181"
    in
    Attr.style targetFragment <| String.concat [ "rgba(", rgbFragment, ", ", opacityFragment, ")" ]
```

So that's all well and good and pretty cool - but you may be thinking:

> Won't typing out long dotted strings get sort of tiresome? Is there any value to having a call site that looks like `Html.div [ colorBuilder.background.op100.normal.secondary ] [...`?

We can go deeper. Check this out - let's create a type alias that makes the `builder` parameter for `AllHues` concrete to a function of type `String -> Html msg`, which is incidentally the type signature of `Html.text`:

```
type alias TextColorBuilder msg =
    AllHues (String -> Html msg)


textColorBuilder : TextColorBuilder msg
textColorBuilder =
    allHues
        (\hue string ->
            Html.span
                [ Color Font Op100 Normal hue
                    |> colorToAttr
                ]
                [ Html.text string ]
        )


type alias AllTexts msg =
    { unstyled : String -> Html msg
    , withColor : TextColorBuilder msg
    }


text : AllTexts msg
text =
    { unstyled = Html.text
    , withColor = textColorBuilder
    }
```

So now we can:

```
dangerousHello : Html msg
dangerousHello =
    text.withColor.danger "Hello, dangerously!"
```

Well would you look at that!


![Sample of text rendered with our custom `text` element](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0d924mwfmsvoenaunlod.png)


Guess what else - we can use the `builder` type parameter to represent a "builder-pattern" function, so that we can use the output of one of our `All*s` functions to apply a value to a wrapped configuration type:

```
type Button msg
    = Button { color : Color, onClick : Maybe msg, label : String }


type alias ButtonBuilder msg =
    Button msg -> Button msg


new : { onClick : Maybe msg, label : String } -> Button msg
new { onClick, label } =
    Button { color = colorBuilder.background.op100.normal.primary, onClick = onClick, label = label }


with :
    { opacity : AllOpacities (ButtonBuilder msg)
    , secondary : ButtonBuilder msg
    , danger : ButtonBuilder msg
    }
with =
    { opacity =
        allOpacities
            (\opacity (Button config) ->
                Button
                    { config
                        | color =
                            (\(Color target _ accent hue) ->
                                Color target opacity accent hue
                            )
                                config.color
                    }
            )
    , secondary =
        \(Button config) ->
            Button
                { config
                    | color =
                        (\(Color target opacity accent _) ->
                            Color target opacity accent Secondary
                        )
                            config.color
                }
    , danger =
        \(Button config) ->
            Button
                { config
                    | color =
                        (\(Color target opacity accent _) ->
                            Color target opacity accent Danger
                        )
                            config.color
                }
    }


view : Button msg -> Html msg
view (Button { color, onClick, label }) =
    Html.button
        [ Maybe.map Html.Events.onClick onClick
            |> Maybe.withDefault (Attr.class "")
        , colorToAttr color
        ]
        [ Html.text label ]
```

You see that? Did you see that?

- We decided that one of our constraints for our `Button` element in our design system was that we could adjust the opacity - so we exposed that via the `AllOpacities builder` record type alias constraint, and made its `builder` type parameter a `ButtonBuilder msg` -
- Which gives us the flexibility to expose the record-alias dot-accessor "picker" for our opacity value - and then apply that value to a function that updates our `Button msg` value!

Check out a call site:

```
sortaTranslucentDangerButton : Html msg
sortaTranslucentDangerButton =
    new { onClick = Nothing, label = "If you push this button, nothing will happen" }
        |> with.opacity.op30
        |> with.danger
        |> view
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/me1bwtph0515fzj49ran.png)


There's still so, so much more that can be done with this paradigm - but let's talk about some things that I've found already:

  - You can specify dot-accessor "builders" for constructors on types, for any type - and then compose them together
  - If you're using the LSP, you get on-dot autocomplete - which makes for a great developer experience with nice call sites
  - By leveraging record type aliases as a sort of "namespacing" - you can put more types and values into the same module without having to smurf[^4], or cut a new module to avoid smurfing
  - You can defunctionalize core parts of an application - for instance, the design system - and separate the call site composition from the implementation - which allows you to colocate related behaviors
  - You can expose `All*s builder` record type aliases and `all*s builder` functions from a module while keeping all constructors totally opaque - which gives you a finer degree of control over module opacity, especially since...
  - Properties in a record type alias don't have to be 1:1 with constructors for a type that it's providing an implementation for

If anybody wants to play with this - there's an Ellie at this footnote[^5].

In closing: I'm over the moon about having figured out how to do this. I'm still struggling to explain it in the abstract, but I think that the biggest key to the question of "what exactly is this Thing" is that its implementation is isomorphic to the design of `elm/url`[^6], which the main guy Wolfgang[^7] pointed out while I was trying to explain it to him the other day.

Un/fortunately, I think that I'm going to have more to say on this as I play with it more and get deeper into it. I'm not sure if anybody else has "discovered" this technique yet, but please @ me on X[^8] or in Elm Slack if you'd like to discuss this further or if you have any additional insight.

[^1]: https://www.refactoringui.com/
[^2]: NB: I'm going to shorten up these types so that my writing doesn't get buried in a fully-fledged design system - which don't get me wrong, I'd love to create - but I need to get this essay done _first_. I've been putting it off for nearly two weeks now, and people are starting to Ask Questions.
[^3]: I just remembered: it's called _CSS_.
[^4]: "Smurfing" is jargon for the practice of adding information to a name in order to disambiguate it from other "things" in a given namespace that have similar semantics but a different implementation. For instance - imagine that all of the code from `Html.Events` was in the same module as `Html.Attributes` - how would you know which was an event, and which was an attribute? You _could_ prefix all of the event attributes with `event` - i.e., `eventOnClick`, `eventOnInput`, etc - but that would be noisy and repetitive, so it's best avoided when possible. ("Smurfing" is so-called after the popular cartoon "The Smurfs", wherein most characters have the word _Smurf_ in their names - i.e., "Papa Smurf", "Lazy Smurf", "Brainy Smurf", "AccursedUnutterablePerformIO Smurf".)
[^5]: <https://ellie-app.com/pMGzWB8GLjca1>
[^6]: <https://foldp.com/blog/elm-url.html>
[^7]: <https://twitter.com/wolfadex>
[^8]: <https://twitter.com/lambdapriest>