upsiflu / less-ui

Write your views across several screen regions, and hide all Ui state in the Url.
https://package.elm-lang.org/packages/upsiflu/less-ui/latest
BSD 3-Clause "New" or "Revised" License
6 stars 1 forks source link

Playing around with Record Type Alias Combinators #59

Open upsiflu opened 8 months ago

upsiflu commented 8 months ago

@wolfadex's elm-weekly newsletter brought this article to me, in which @jmpavlick showcases how a record parametrized over a builder-function can produce builder compositions in a stunningly beautiful fashion.

I want to document my process of understanding and reproducing this gem here.

Because I have a hunch that if can make the code of the Ui type (and perhaps also the Layout type) much more self-explanatory. Plus, I had thought about adding builders in the Url codec as well as in the application module, and RTACs look like supercharged builders: a builder itself produces a record of chained builders.

upsiflu commented 8 months ago

In the following comments, I'll quote from John's article on dev.to and add my understanding underneath as my playing-around continues.

upsiflu commented 8 months ago

Scope of RTACs

you can generalize and combine properties on record type aliases.

upsiflu commented 8 months ago

John's example - a Color Theme

So the use case is: we have a set of dimensions modeled as sum types that constrain the color of an element.

Step 1: Model Color as a Product type

Each dimension of a Color is a sum of its members: Mode = LightMode | DarkMode

Color is the product of its dimensions: (Background | Font) , (LightMode | DarkMode) , ...

You can model it as a Product type:

type Color = Color Target Mode

Step 2: Instead, model each dimension as a record type over a generic type parameter builder

This way, we can call a constructor with the convenient dot-accessor syntax in elm.

Plus, we give each constructor an unconstrained type builder.

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

Step 3: Generate a record of builders per target, given a function that turns a Target into a builder

type Target = Font | Background

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

Step 4: Leverage ❤️function composition❤️ to compose builders

type alias AllColors builder =
    AllTargets (AllModes (AllAccents (AllHues builder)))

Through alias substitution, any AllColors instance will be a record { background, font } where each field is itself a nested record of { mode₀..ₙ} etc:

{ background: 
    { lightMode : 
        { accent₀ :
            { hue₀ : -- here we want the final color to be defined.
            }
        }
    , darkMode : { ... } 
    }
, font : 
    { lightMode : { ... }
    , darkMode : { ... } 
    }
 }

We can now compose dimensions to form those nested record types.

Step 4: Provide an implementation

Color is a constructor function that accepts values for each of its dimensions Target, Mode, etc.

AllColors Color will be a record with all possible permutation of Color.

We can simply assign the selected record field values to the constructor parameters like this:

colorBuilder =
    allTargets
        (\target ->
            allModes
               (\opacity ->
                   allAccents
                       (\accent ->
                           allHues
                               (\hue ->
                                   Color target mode accent hue
                               )
                       )
               )
       )

Now that we have a builder for a "semantic" color, we can implement a single implementing function for a concrete value in CSS:

colorToAttr : Color -> Html.Attribute msg
colorToAttr (Color target opacity accent hue) =
    [...]

Step 5: Apply the composable constructors to a builder

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

Then we can implement create, with.xy or withXy, view as we do in any other builder-pattern module.

jmpavlick commented 7 months ago

I'm so sorry - I don't have Github notifications "on" in any meaningful sense, and I just now saw this.

I'm so stoked that you found this interesting, and I hope that you find it useful - I've been playing with these myself, as time allows, and I hope to have more to share soon! ❤️