Tune in to the tools and techniques in the Elm ecosystem.
The Builder Pattern
We discuss the tradeoffs of using the builder pattern and how to get started with it.
July 13, 2020
What is a Builder?
Init a builder data structure with a set of reasonable defaults
Customize those defaults through a series of chained function calls
Builders have the same type for the return type and final argument, so that they work nicely in a pipeline
Resources and Examples
Robot Buttons from Mars
elm-review package docs
elm-pages/phantom builder live coding
Richard Feldman's Oslo Elm Days keynote
Hello Jeroen! Hello Dillon! How are you doing today? I'm doing pretty well. It's warm here,
so I'm enjoying the balcony and it's nice outside. Yes, me too. That's been a very pleasant getaway.
Yeah, escape. Yes. So what are we talking about today? Well, today we're gonna get a little bit
of your wisdom on a topic that you've put a lot of thought into and I am probably gonna have a
lot of questions for you because we are talking about the builder pattern. Yeah. No phantoms today.
No phantoms. Maybe another day, but just plain vanilla builders and really you don't see it used
that much in Elm right now. No, we don't. I think we mostly see it in applications, but we don't see
much in packages. That's a good point. Yeah. Yeah, whatever is best for the package API is good enough,
I guess. But yeah, I use it mostly in applications. Okay, okay. I am very curious to dive into that a
little bit later on. Why don't we start with a definition? So how would you define the builder
pattern? So the builder pattern is a pattern that you use to work with highly customizable elements.
So you start with a good set of defaults, and then you customize parts of those defaults, you
override those. So if you're building a car, before you can drive that car, you have a blueprint for
it a schema. And there's a lot of ways that a car can be customized, it can have different colors, it
can have different brands, the wheel can be on the left on the right, you can have a six shifts, you
can have the other manual automatic thing, whatever you use in the US. So there's a lot of things that
you can customize. But you have a good set of defaults in a car and like, yes, usually has four
wheels, the driving wheel is that how do you call that steering wheel steering wheel? That's what I
call the wheel before. So the steering wheel is usually on the left. And you usually have four
doors and one for the for the trunk. So what you usually do in Elm code when you use this pattern
is that you start with a function that I usually call new or give it the same name as the thing. So
let's say car dot new, and then you use the pipeline operator and set one piece of customization. So
you do car dot new, the pipeline operator, car dots with color red, and then another pipeline
function for each step. So with color red with steering wheel, right, etc. I sometimes see it
named as the width pattern because you have a lot of width functions because that is what the what
makes the most sense orally. You can just read it car with color red car with steering wheel on the
left. But I don't think you need to be dogmatic about calling functions with something. One thing
that you'll always notice with the builder pattern is that it's going to return whatever type of
builder thing at the very end. And it's also going to take that as the very last argument because
it's always it always involves chaining in the context of Elm. It just makes sense to chain it,
right? Yeah, you start with the car and you end up with the car for every function. Right. So so
you would maybe have like a car builder type in your example that you're talking about. And with
red, the type annotation of with color would be that it takes an argument of a color and a car
builder and it returns a car builder. Yep. That is a good example. Another example that people may
have encountered is Luke Westby's HTTP builder package, which we've mentioned on the show before.
You know, it's it's a nice application of that pattern that you you know, as you were saying,
with with a builder, you sort of have some sensible default. So when you initialize a
builder, you could immediately construct it, or you could continue tacking on options and modifying
things. So for example, you could initialize an HTTP request with, you know, with the URL. And
you could just perform that HTTP request at that point. Or you could continue adding on headers
and customizing the timeout, you know, perhaps by default, there's no timeout or, you know,
perhaps if it's your own application specific one, you know, maybe in your application, you don't even
allow to customize the timeout. And you just have one application specific timeout. Maybe in your
application, you have a default timeout that you choose for your application, but you allow it to
be customized and maybe even put constraints on the possible timeouts and you know, choose your own
states for that. But that's another example of a builder. Yeah, it's a really nice one. I don't use
it though. I kind of use that same pattern, you know, I kind of used Luke's package as a as a model
for that in Elm GraphQL, you send HTTP requests with a builder style. So there's there's another
thing that seems like a really important piece of the builder pattern, which is that you sometimes
are describing certain things that have to go together. So for example, if you do an HTTP
request, you need to define a decoder as a part of that HTTP request. So Luke's HTTP builder package
gives you HTTP builder dot get. And it actually doesn't have a decoder, which is kind of interesting.
So you just do HTTP builder dot get, and you give it a URL. And that's all you need. But there are
certain times when two pieces need to fit together. So for example, the HTTP builder package has this
with expect function, right? So you you apply that in your chain, HTTP builder dot get some URL, pipe,
you know, right pipe with expect, and then you give it some HTTP expectation. And that HTTP
expectation sort of contains more than one piece of information. Like it's not just a JSON decoder.
It's a JSON decoder that tells you how to turn it into a particular type. And you've now transformed
it into that type. Or, like, for example, if you're, you know, with your example of a car builder,
maybe if you have like a left hand drive or right hand drive vehicle, maybe that also implies
that you have a steering wheel that maybe there's like some regulation about the license plate type,
that the space for a license plate is wider in Europe, and it's less wide in the US. And so so
maybe that's part of the init that you do like car dot init American car. And now you've admitted
it and internally, it's going to say that it's left hand drive. But is it called left hand drive? I
don't know if it talks about what side you sit on or what side you drive on right hand. I don't know.
So a US car drive, we drive on the right side of the road. So the steering wheel is going to be on
the left of the car on the left of the car. And the license plate is going to be for an American
license plate. But if you initialize a builder for a British car, then you're going to have the
steering wheel on the right side of the car. And you're going to have a European style license
plate. And you want to enforce that you don't you want to make impossible states impossible with
your builder. So you want to eliminate the possibility that you have this invalid set of
configurations in the in the API you expose. So if you said that you could initialize it, and you
could say, car builder dot new, and then pipe that to car builder dot with left hand steering wheel
for the US, and then you say car builder dot with European light license or vice versa. If it's if
it's a British style steering wheel, you want to enforce that it's going to be a European style
license plate. And so you don't want to separate those two things to allow somebody to initialize
it with a British style steering wheel, and American style license plates, those two things
shouldn't go together. So you want your builder API to enforce those configurations that are valid
Yeah, definitely. So yeah, usually what you would have is one with function that would take the
information of what license plate you have, and where the steering wheel will be. Yes. So you
wouldn't be able to mix those together.
Exactly. Right. Right. Because those two configurations are not independently variable.
They have certain constraints with each other that you want to enforce. So you could potentially
you could have a custom type that enumerates those possibilities where a right hand steering wheel
takes no options for what type of license plate you have. And a left hand steering wheel, that
variant for the custom type takes an option for is it American or European style license plates?
Yeah, you could also have that in as a type variable in the car builder type, a phantom type
or a regular type.
That's right, we could we could use, we could use the phantom builder pattern to enforce that as
well. And that opens up some new possibilities. And that's that's topic for another day.
Yeah. But just as a reminder, that's this is a pattern that I quite like, but it's just one
tool in a whole list of other tools in your toolbox. So this can be used for for certain
cases, and might also be the wrong abstraction, the wrong tool for other use cases. But I think
that using this one with other tools is very useful. Just pick the right one for the right API.
Right. So okay, so let's get into what's valuable about this technique. What problem does it solve?
Or what types of problems does it solve?
Yeah, so Brian Hicks did a talk at Elm in the spring, I think, yeah. Mm hmm. Called robot buttons
from Mars, which I found really good. And I keep mentioning it when I talk about the builder
pattern, where he goes through a lot of ways that you can build customizable elements. And he ends
with the builder pattern saying that this one is pretty good. And he lists what is good with every
pattern that he showcases. And the main one that I remember is consistency. The problem that
you're trying to solve when you use the builder pattern is that you have a highly customizable
elements. So in Elm applications, that will usually be UI or often in my case, there will be UI. For
instance, you're designing a button. And the button can be very customizable. You can have an
icon on the left on the right. It can have text or not, it can do something when you click on it,
it can be disabled, etc. You can have lots of state for it, can be big and small. And the thing
is, you don't want to recreate a button in your code every time you need a button. Just HTML dot
button with a bunch of attributes thrown in. Mm hmm. Yeah, you don't want that because then it will
not be consistent UI wise. Yes, across the application, it will look different on this page
than in this other page. Right. And if it is consistent now, then it's not going to be
consistent next week or next month or next year when you make some changes to one of the buttons
or by the next developer or yes. So that is the main problem that we're trying to solve here, I
think, with UI elements, for instance. Yeah. So you basically start with a good set of defaults,
and then you limit how you can customize it. For instance, you will not be able to say that it will
be 800 pixels wide, because you will not allow that you will not say with size 800, you will say
with size big or size small. So you limit what customizations it can have what values it can
have. Right. Yeah. So I guess on on the one end of the spectrum, there's obviously just chaos,
no constraints, complete chaos, and you can just build the custom button yourself anytime you want
and you can customize it anyway. In fact, it might not even be a button, it might turn into a link
or now you're presenting, you know, ways of interacting with these button like elements in
your app in, you know, a very scattered way. So that's not great. Or you could impose too much
uniformity and say, here's a button function. And cause always like this. And it always looks like
this. And of course, eventually, somebody's gonna say, Well, what if I want to disable this button?
What if I want? What if I wanted not to say pay? Right? What if I what if I want it to have like a
different highlight color. So there's a certain set of constraints that you want to enforce where you
say, you know, we have a color for warnings and for actions and for buttons that aren't, you know,
attention grabbing, but they're there that aren't like a default option that you want to draw the
user's eyes to or whatever. Okay, so that's, that's good to have those constraints that in a way, it's
almost like defining your design system that Yeah, you know, it's a nice tool for that, right. And
that design system could be for actual visual layout elements, or it could be for the range of
possibilities for how you perform HTTP requests within your application, for example, those are
both good applications that so now I guess another option is if you wanted to enforce those
constraints and have a single point of truth for what the possible options are. Another direction
you could go is just have a giant record and say, here are all the possible options, you know,
whatever, whatever data structure, right, it could be a record, it could be a custom type, it could
be some composition of those two data structures. And you just say, well, I've kind of made
impossible states impossible with this data structure. And here are all the internals of it,
just good luck, you know, I've eliminated the possibility of you writing something that's
invalid with our design system, but you've got to build it up explicitly every time. So it solves
that problem too of having to write every single thing explicitly every time. Yeah, internally,
that's what you will have when you use the builder pattern. But you will hide that complexity from
the user. Yes, it's a good balance between consistency reusability without it being annoying.
Right, right. So so like, in a nutshell, it feels like what the builder pattern is doing is it's
giving you a starting point that's valid, and then giving you a way to extend that starting point.
So you start from maybe like the most common, or the most intuitive default, and then you extend
from there with a sort of constrained set of ways to extend it that is defined by your design system
or your API. Yeah, it can be a bit tough to find that good defaults but you can always change it
later. It's not that bad, I guess. Right. Unless you have thousands of buttons. Yeah.
One thing I think we didn't explicit enough is that sometimes you don't have good defaults.
Or sometimes you can't have defaults for certain things. For instance, if you try to define a
button that always has an onClick handler, you can't have a default for that. So what you need
to do is pass it into the new function, the button.new or button.button function, and that will
be added to the default configuration right off the bat. And then you go and customize it with
the with functions. Right. Yeah, sometimes you can get clever about that. But sometimes it's,
there's not a good default. So maybe I'm jumping ahead a bit. But I was pondering this question
when we were talking about this topic. And I was thinking about this question of why does the
browser API in Elm not use a builder style? Because it seems like a pretty common thing that,
you know, you like open up a new Ellie to create an example. And it starts with a sandbox and you're
like, okay, well, I don't want a sandbox. I want browser.element. Or I want browser.application.
Or I guess an Ellie browser.application doesn't really make sense. But it doesn't work. Yeah,
yeah, it's not really going to behave the way you expect. But you know, the point is, like,
whatever it may be, whether it's an Ellie, or pulling down an example, or a starter repo,
or something like that, you know, it's a pretty common thing that you see people saying, Oh,
this is convenient, I can create an app really quickly and simply if I do browser sandbox,
you know, or if I start with like, just main equals text Hello, then that's a valid Elm
application that it will accept. And, you know, I've wondered, like, is there a nicer way to do
that where instead of having to just say, okay, I have a browser sandbox. And so in it is just
a model. There's no command, it's not a tuple model command. And it doesn't take flags, it doesn't
receive any flags. Yeah, now I want that. And I want subscriptions. And how do I, you know,
incrementally move to that instead of just saying, okay, now I want it to be a browser.element. And
then having 20 error messages that you have to fix. I mean, it's fine, like you're going to be
able to fix it, it just seems like it could be a smoother upgrading process. Yeah, yeah,
it definitely feels like it could, but I think Elm has gotten it quite right, actually. But let's
let's try to make it a bit better and see how it goes. Maybe. Yeah, so I actually played around
with it a little bit. And it gave me some good insights. And one of the things I discovered,
so, you know, you made this point about sometimes there are good defaults, or sometimes there are
not good defaults, or, you know, like for a button click action, what's the default, you can't really
have one because you can't just have like, no message type, right? You can't just say, by
default, on click, there's no message, you need some sort of message. So you could like take a no
op message, or whatever if you wanted. But I was thinking about this, you know, with a browser
application, you have like on page change, you have some of these messages that you register,
right? I was thinking about it, like, what would a default be for that? And it's similar, you need
the type of it to be message. If the type of it is not the same message type that your update function
receives, then things won't line up with the types, right? But what if since you're controlling it,
since you are creating the application, you could have it be a maybe message? Yeah.
Under the hood internally in your builder. So when you register a, you know, a browser application
builder, you know, let's call it an application builder module they were using to create elm
applications. That sounds very Java. Let's call it an app builder. Yeah, app builder dot elm. So
let's say you're trying to build a browser application with this app builder API. And then
you could say, well, I'm not going to require you to give me an on page change message to start.
What you could do is internally your data structure that you use for your app builder type could be,
it could have like a maybe on page change message. Yeah, or on maybe on page change function for
that, right? Which the browser program probably has underthrow it actually. For the elements
programs, it probably has something like that. Right, it could well it could well have something
like that to give you these abstractions of browser dot element versus browser application,
right? So then what you could do is when you say app builder dot to program, that's actually going
to take your app builder type and actually build it, right? That's what the builder package is.
That's what the builder pattern does it. It creates the blueprint. And then eventually you say,
okay, now I want you to actually build it for me. And then it builds it. Yeah, I want to use it.
I want to use it now. I'm done building it. So when you say I'm done building it, you say
app builder dot to program, and it gives you a browser dot program. Is that what it is? It's a
program. Is it like a platform dot program? That's on program,
remember, platform dot program, then what it could do is internally, the app builder
could take care of turning that nothing into it could wrap the message type. So the browser dot
application, the message type would be actually maybe user message. So it would wrap it in a maybe
and then it would internally control if you hadn't wired up the on page change message,
it says fine, I'm just gonna pass nothing in that case. Or if you do have a message for that,
it says, okay, I'm going to pass a just message for that. Yeah, the question is, do you really want
that? Do you really want a program where you get the problem that you're trying to solve here is
what if I don't pass an init? What will a program do if I don't pass an init? What if I what if it
doesn't have an update? What does what if it doesn't have a new URL change? Those are the
questions they're trying to answer. Right. Like we talked about these these valid configurations,
like a British steering wheel must always have European license plates. Yeah, kind of. Yeah. And
I think the answer here is there really isn't good default that you can have a good fallback.
So if you did go with this build a pattern approach here, you would probably put the init
into the new constructor, you would do app builder dot new, you wouldn't pass in the init,
you'd also pass the update, and you'd also pass the view if you're talking about a browser program.
And what you kind of ended up with then is what the API already looks like. So the kind of the
only thing that you would gain is that you don't have to specify subscriptions, which is annoying
sometimes to specify when you don't have any, but it's not that bad. You don't gain much from it.
Right. Well, you don't have to specify subscriptions, and you don't have to specify
on URL change. And yeah, and there's also one other thing that all those browser functions do.
They make those functions simpler when you can. Exactly. And that's that's the problem I ran into
when I was starting to do that, I realized, oh, browser dot sandbox, the update function needs
to match the init for a sandbox, because if you can't perform commands, then
I know that would be okay, I think, because you don't need the updates to necessarily return
commands, even if the init does. That's true. One thing that you will, that is a bit weird,
is when you do an init with a simple init. So you would probably have different variants,
actually, you would have with simple init, which just returns initial model. And the init and
update would probably have to match, right? So when you declare an init. I don't think you have to.
I don't think you have to. But I guess you're right. Oh, interesting. Yeah, I also thought with
the subscriptions should match the update, but no, they don't. It doesn't really matter. Yeah,
because you can just use the default. Yeah. Yeah, yeah, you can just map it to add a command none
at the end. That's a good default. But one thing that is kind of tricky is that you would have
on URL change that kind of already assumes that you have an init based on the URL. So when you
look at browser application, the init function that you require, it takes flags and the URL.
But what if you do with simple init, which just returns an initial model, maybe initial command?
Well, it doesn't depend on the URL. So you will likely run into some weird states. Right. So what
you probably would have to do is group those together, the init and the URL change and the
other handler. Right, which is a bit annoying. Right. And then at that point, you have a
discoverability challenge where, okay, you've technically made it so that you can only make
you can only make configurations that make sense where it's like, if you're registering an on URL
change, you're probably going to want to have access to the URL in your init. But then how do
you discover those possibilities? And have you actually saved anything from from the design that
that browser currently has? It is an interesting question, because I wonder, like, is there really
that much value? Like, if you're if you're making a builder style API in general, would you ever
make arguments optional in certain variants? Because it seems like, in that case, like, just
ignore those arguments as the starting point, you just say, I guess maybe the browser API is in a
certain way optimized to make the learning experience nicer for a newcomer where they are
introduced to concepts gradually. So if it's the kind of thing where if we were regularly
going back and forth between these different values in a professional setting, rather than just
like, playing around with examples for like learning or things like that, then it might make
more sense to be able to like, change it fluidly, but it's really optimized for the learning path.
And it's nice to be able to separate to say like, okay, and it and it is just what do you want the
initial model to be and for a, you know, for somebody who's brand new to Elm, that's a nice way
to learn the Elm architecture where you say, here's a counter app, you say, and it initialize the
model to zero, and then you have an increment message. And in the update function, when that
message comes in, you take your current model, and you add one to it. And then in your view function,
you do the and it's, they don't have to even look at any of that complexity. So even though
even though ignoring arguments doesn't really make things simpler in the context of the Elm guide for
beginners, it does make it simpler. Yeah. So I guess that's a bit of a special case, maybe.
But that's part of API design is how do you make the experience nice for beginners and make it easy
for them to learn the concepts. So in Elm review, I also use the builder pattern quite profusely.
Yes. With some phantom types in there. I have a concept of visitors that you add to the rule
schema. And for every one of those, I have a simple variant, where it takes less arguments,
and that it helps with the learning curve. Right. And keeps things simple, as long as it can be.
Yeah, I mean, it's a bit of a trade off, right? Because if you introduce too many, then it can
muddle the waters and it can make it confusing for newcomers. So there's another consideration too,
which is so with Elm pages, I actually have a builder right now for the Elm pages application
where you can chain on a couple of things. And I've got some, you know, you and I, your own did a
live stream where we kind of went through and played around with those ideas in that context.
And I think it's a pretty good pattern in some ways. But actually, one of the things we discussed
in that live stream is that in the context of Elm pages, you have to consider the ramp up
experience that most people are going to have. And really, the ramp up experience that most people
are going to have is that they're going to initialize it from a starter repo. And whether
that you know, maybe Elm pages at some point introduces an Elm pages init command, which
gives you an app, or maybe it just continues with a starter template like it has now. But either way,
you're probably not going to wire it up from scratch. It just it wouldn't make sense for that
project to build something completely from scratch. Like it might with Elm, like with Elm,
you're probably going to run Elm init to get an Elm.json. But then you open up main.elm. And you
just say module main, you write it out by hand, right? It's it's not not a big deal.
Yeah, or you copy paste the le example. Yeah, counter example. Yeah. Right. So I think you have
to consider how are people going to use it? And how do you find an API that makes it the easiest
to use in that context? So the cause of Elm pages, when the way people are going to use it is by
cloning a starter repo, it's actually kind of nice to have subscriptions model equals sub dot none,
like that there's a function somewhere called subscriptions instead of, you know, because now
in terms of discoverability, they have this placeholder that they can now change. Yeah. And
there's value to that placeholder because there's a discoverability benefit when you clone the starter
repo. You can see all these placeholders, you can see the types, maybe there's something that
surprises you that, oh, I have access to this data here, I didn't realize like, that's not what I'm
used to with browser dot application. So there's a certain benefit to that. Yeah, it's a trade off
between knowing that things are there and having to specify those things that you don't care about
and that you won't use. Right. And I guess if you're only specifying something once, right,
if you clone the starter repo, and it's already specified for you, and you're never going to
specify another one, then what benefit is there? But if it's like a button, and you could have a
thousand buttons in your app, then it's going to be a problem if you're creating it by hand every
time. But so you have to you have to consider how it's used and how often it's used. Definitely. I
do think that Elm pages is quite a peculiar example. Because yeah, it's where you copy paste
it from somewhere. Right. I think the usual use case will be something like the button where you
don't want the record with 20 fields that you may or may not want to use. And that is the core
thing that you want to simplify with the build pattern or with something like it because there
are other techniques that you can use. Imagine the HTML API, the Elm HTML API, where you had to
specify every possible attributes and say nothing to most of these. Like just no, just that is
something that you want to avoid. Oh, man. That doesn't sound fun. Elm is a fun language. Try this.
Or you just have to copy paste this thousand lines of code to do one button. It's very explicit.
Oh, God, yes. I mean, I guess that does get at another. I think this is good, because we're kind
of picking it apart and poking at this core concept to ask like, what really is the builder
pattern? And why would you want to or not want to use it? And I think one of the things that is
clear is that its intention is to sort of abstract how you build something. And when you abstract how
you build something, what that means is that as your API evolves. So, you know, if you've got your
design systems team, that's, you know, you've got maybe you've got like 20 teams that are working
on this application. And you've got like one group of, you know, designers and developers working on
a design system for everybody to use, then well, if your API is a record, and now you add a new
option where you can configure this new thing for a button or a modal or whatever, right? Then
then everyone will hate you. Or they'll just force you to upgrade it for them, which they'll probably
still hate you because now when they're reading their code, you added another default option in
there. Oh, no, now we have a git conflict. And there's, you know, sometimes it's confusing if
if there's a default option, you could have it be nothing, which is noisy. Or you could have
the default option in there explicitly, which now it's not guiding you towards using that default.
And it's not a default anymore. It's no longer default. Exactly. Yeah. So it protects an API
from change, it encapsulates how something can change in a way that just exposing all the options
to a function doesn't doesn't hide it that way. Yeah, that also means that you can just refactor
things way easier. We to go back to discovery problem, most people I think are using an editor
with auto completion. So when you do button dot with, then you will see the list of things that
you can apply. Yeah, you can discover things that way. You can read it just by looking at the code,
you'd have to interact a bit with it. But I think it's okay. I haven't run into too many problems
with that. Yeah. And that's another good argument in favor of that naming convention of starting
the options with the word with. Yeah, that is a good point. But I do still remove the width if
it doesn't make sense. Like, hmm, I do button with on click, but button disabled, because with
disabled doesn't make much sense, in my opinion. Very interesting. The way I do it is that try to
read it. But a new with texts with color with size, big stuff like that. It's a readable pattern.
Right? You go with disabled because that's what reads best with not with disabled. Right. You know,
I actually occurred to me that there's nothing preventing a consumer of a builder style API
from doing it without pipelines without pipes. Oh, yeah. Imagine just a giant one liner with
parentheses all over the place. Yeah, if you don't know about the pipeline of prayer, then
learn it before you use this pattern. No, give examples. Let them know about it. Yes. Yeah. In
your documentation, use it. That's also one thing with discoverability. You want to document it.
When I define the new function, button.new, I usually have documentation with it, because that's
a good practice. That's helpful for your teammates. And in that one, I give an example of how you're
supposed to use it. So you would have button.new and then button with on click. So you give an
example and a feeling of how it can be used. And then the user will go through the file and
all the documentation and see, oh, I can do this. Oh, I can do this too. Yeah, that's nice. Right.
And they should be meaningful examples. Yeah. Not just foo. Yeah. Like if you're building a button,
then think about an actual use case for a button and use that for the example. Yeah. Like pay
to radio. Make an HTTP request with the HTTP builder that sends money to radio.
Good. That's a good example. Or just hard code that option in there. Or just make it so that the
with functions don't do anything and you stay on the pay option. Whatever you do, whatever you try
to customize, it will just be pay. I like it. I like it. I think if someone wants to get started
with the builder pattern with actually writing it for their application, one thing that I think is
helpful for starting with this is there's actually a very natural way to transition an API that's not
using the builder pattern to using the builder pattern, which is, you know, let's say you've got
a record that is taking all the options for your button. And maybe some of those options are maybe
values and you give them defaults, right? Then you usually want to start with the record or
something like that. Yes, exactly. Because that's what you will use internally. Exactly. So if you
take that record example, you would just copy that record that you have in a type annotation for,
you know, your button function. And then you would copy that record. And now that's
going to be your internal builder data structure. And you probably want to make it an opaque type.
So you so now you'd have, you know, button builder dot elm, or maybe you just call it button dot elm.
And now you you wrap that in an opaque type. So it's like type button builder equals button builder,
copy paste your record there. So now you've got the internal representation. And you initialize it
to just exactly as you would initialize it with the defaults, you can put nothing values in there.
You can, you know, when you finally construct it, you can turn those into their defaults. And you
know, that's that's the starting point. And then you could initialize it with that exact record as
a starting point. And then little by little, I mean, now you can commit that right, it's like
working code, you just need like something to initialize it directly with that record,
and then turn that opaque type into an actual HTML message or whatever type. Now that's like,
you can commit that you can use it. Maybe you don't want to commit everything right away though,
because now, well, yeah, all of the consumer, no, you should commit it, you should commit it.
And all you have to do the only change you have to make is all the calling code needs to now call
button builder dot to HTML to HTML. Mm hmm. And now one by one, you take those, you know,
maybe values, you take one of the maybe values, like maybe icon, and you pull that off of the
record that you in it with. Yep. And you turn that into with icon, and it no longer takes a maybe
with icon takes an icon, not a maybe icon. Yeah, because the default is nothing. Yeah. So you move
the the field from the record, and then you go everywhere in your application. And where it was
used, we specified something that was just an icon. Yeah, you add the width icon function function
modifier. Yes. And the other places you just remove it, but you have to go through every
place where you create a button. Exactly. So it's safe. It's a safer factory. It's safe. And you can
do those one at a time. So for each value that you're passing in a nothing or adjust, just do
those fields one at a time, little by little, and you can commit it at each step. Yeah, so that helps.
Is there anything else people should know getting started with using the builder pattern? Sometimes
you start with others with another API than the record. So you have multiple functions or multiple
arguments. So if there are multiple arguments, you can just transform that in in a record in a way,
or at least conceptually. So it's not very different. Yes. If you have multiple functions, like the
browser API that we discussed before, then I think it's necessary to start seeing the similarities,
but also places where they differ. When you have different functions, what you really want to
look at is which fields are interconnected. Yes. So like we talked about browser application,
in it and on URL change are kind of linked. So you probably want those to be one record before
before you move that into the bigger record. You wouldn't have a giant record with in it and next
to it and URL change would have in it and URL change be a record. So you would have a giant
record containing in it and on URL change. Right. Because those two are interlinked.
Yeah, exactly. So that's what you need to look out for. Yes. Right. I mean, on a similar note,
I mean, the name of the game for designing any API, whether it's an internal one for an application
or package or whatever, it's the same idea that you're trying to make impossible states impossible.
You're trying to model the constraints of your domain. So you want to be on the lookout for that
and you want to, you know, don't make things configurable. That shouldn't be changing.
As you said, don't make things independently configurable that need to be able to be
constrained by each other. And also, you know, don't be shy. We discussed this on another episode
that don't be shy about building your own internal API. That's not a generalized API. It's actually
it's good to build a custom tailored API for your own company or code base or whatever,
whatever domain you're in, build the API that you need and make assumptions and constraints for
your use case. And that will make it a nicer experience to use. It'll be nicer for the
discoverability because the types kind of make it more clear what the possible values are.
That's going to make you feel more confident as you're making changes and working in the code base.
So as with any API, you want to be thinking about the constraints as you're structuring your API.
Yeah. And if the builder pattern doesn't give you those constraints, then use something else
along with builder pattern or not. Another thing I've noticed with APIs that use the builder pattern
is that sometimes you might have, you know, if you have something like take the HTTP
versus the HTTP builder APIs, the HTTP API just says, give me a list of all the headers, right?
Cause it's just a single function that you call to get this value. So the HTTP builder can say
with header. So you can now take what was a single list of headers and you could chain it with header,
with header, with header, with header. So that's another thing to keep in mind. And
there's no right answer. It kind of depends on the context, right? It could be that you want to have
with header and with headers where you let the user pass in the list. They could both be useful.
Or maybe you want to enforce one or the other. You really need to think about what's going to be
intuitive, what's going to work well for how users are going to use it. And you don't want to overload
the user with every possible permutation or every possible option they could have.
Yeah. One thing that most with functions, they will override the underlying customization,
but yeah, you can have them adding up like with headers, but I think that most of them will probably
just override things. That's a really important one. Right. If you have with header, you're
probably going to want to tack on another header instead of just saying there's one header. But in
cases where it's like with color, if you do like button.withColor, if you call withColorRed and then
withColorGreen, you're not going to get a Christmasy red and green button, probably.
Although it might be nice. Well, you could have the first call set the primary color and the second
call set the secondary color. Right. That would be very confusing. Yeah, I would really recommend
saying with primary color with secondary color. Yes. Every modifier should be readable on its own.
It shouldn't have to depend on the context of what has been set before or will be set after.
So yeah, it's kind of like the HTML API. You have a list of attributes. Oh, HTML. Okay. Yeah.
Let's talk about HTML. A non builder API. Right. Non builder API, which uses the list pattern,
which is in practice almost the same. It gives pretty much the same benefits, just the syntax
is very different. Yeah, you can just read one item in that list and that should be true. You
shouldn't have to say, oh, this one is invalidated because I did that before, did this other,
said this other attributes afterwards. Yes. Yes. Yeah. I think maybe a good rule of thumb
for overriding versus not would be like, yeah, don't confuse people. And if you can add unlimited
options like headers, then if you say with header, with header, with header, with header,
it makes sense that you just continue adding those headers. But if you say with color, with color,
with color, with color, it seems like one of those calls should bump out the other one. And in the
case where, you know, like you said, in the case where you have two colors, I mean, I would think
maybe you even want to constrain those two together, right? Where maybe you have like a
palette of colors that if your primary color is some dark, you know, color, then the other one
should be a light color or vice versa. And you could even constrain the, and maybe with color,
take some color palette value that you've defined in your application that defines the possible
primary and secondary colors that can go together. And that's defined in a single place and you can't
just arbitrarily combine ones. It's, there's a source of truth for the, for the color, you know,
primary, secondary color combinations. Yeah. You're adding constraints to gain consistency.
Mm hmm. And you're declaring those two values in a common place, like you said. So in order to
make sure, you know, understand what's going on, you can look at independent things independently
and things that are interconnected, you should see in the same invocation of with color palette.
Yeah. Why aren't there more uses of the builder pattern in the Elm ecosystem?
As packages, you mean? Yes. I think the reason is because there are not that many highly
customizable things. As I said, the build pattern works really well with things that you can
customize a lot. And there are just not that many things that you can do that for. Like you don't
publish a package to build a car or a button. All the examples that we chose and for which it worked
well are application specific or... So the HTTP builder one is a good example of where it works.
Actually, as we've mentioned in a previous episode, you know, Richard Feldman gave a nice talk,
keynote at Oslo Elm Days, where he kind of talked about, well, HTTP builder is nice,
but what's nicer is having your own domain specific HTTP builder that can make assumptions
about your application. So even that one is even better to use that pattern rather than that package
and apply that pattern to your own internal APIs. Yeah. Another reason is that you want people to
avoid falling into pitfalls. So you try to make impossible states impossible. And the thing is,
with an API like the builder pattern or the list pattern, the HTML like pattern,
there are a lot of things that can go wrong. Like you can specify things twice and that is confusing.
You can, and then there's certain set of problems with that that you just can't solve with the
builder pattern. So people won't go there. They will not use the builder pattern for those problems.
Right. Cause if you say HTTP dot with timeout and then with, you know, with timeout 60
and then you say HTTP dot with timeout 30. Yeah. Which one will it take? Which one does it take?
Maybe 30, maybe 60. Right. Right. So, so there are trade offs there in terms of the predictability
and simplicity. Yeah. But there are a lot of constraints, especially that you can't implement.
Yes. You can't enforce. I don't really have an example. Like, yeah, say for instance,
in Elm review, you can build a rule, but you could add no visitors, which is the single
most important element. And if you have none, it's not useful. Right. So that, that wouldn't work
with the builder pattern. All those constraints do work when you add phantom types. Is this a
teaser for part two of this theme? Even the example where we try to use the builder pattern on the
browser API, there are a certain number of constraints that you can't enforce. If you add
phantom types though, you can. And that I will probably talk about at some point here in a blog
post or a conference. If you want to hear about that, hopefully a lot of those, if you want to
hear about those, let us know. And maybe we'll, I will at least rush a blog post.
Oh, the people want it. You're in the people want to blog posts.
They don't tell me. They tell me they want Elm review. So that's what I'm focusing on.
That's fair. That's fair. But yeah, I think that is one of the problems that
why it's not used in a lot of packages. You can't enforce a lot of constraints.
Right. Yeah. And I mean, ultimately, you know, as, as, as you've said, it's, it requires your
judgment, use your judgment to figure out which API design is going to be best. And it's not like a,
you know, oh, now I've found the silver bullet for designing APIs.
Now everything is going to be a builder. That would be very bad.
Oh no. But if everything were phantom builder, like there are always trade offs, right? To any
of these things like it's a powerful technique, but if everything becomes that it would be a pain.
And ultimately, well, maybe another thing to call out is just that it might be tempting to substitute
using a builder pattern for your internal or external package API, rather than being thoughtful
about, should I really include this in the API? Does this really need to be extensible and keeping
it as simple as possible? Because sometimes the answer is just don't assume that, you know, you're
anticipating future needs too much. Just focus on what you need to do now. And you may be surprised
that what you think you need ends up evolving. So start very simple and don't add complexity
preemptively. And, you know, using a builder pattern is definitely a preemptive type of
complexity that it has a cost and, and hope, you know, in some cases it's worth it because it,
it turns out to be very nice, but you can't beat simple. If you can find a simpler thing that
doesn't even need that solution to that problem, simple is best. Yeah, if you can only customize
one piece of something, you can just use one argument. That's right. Yeah, go for the simplest
thing and yeah, judgments. Just try it out in certain cases. If it doesn't work out, we factor.
Mm hmm. Yeah, that'll be easy. It's Elm. Maybe one, one last thought I had on this topic is,
this is a more advanced technique, but sometimes I find it useful to be able to like map functions.
So in certain builders, you may want to have a function as a default, right? So like maybe you
have some function that returns a list of something and maybe you can apply something in your builder
chain that adds things to that function. Like, I don't know, maybe you're well, like if you're
doing a browser builder and you're adding subscriptions, you could say with subscription
and you could take in a subscription and you could call it multiple times and include that subscription.
Okay. Yeah. And a subscription has access to the model. So one way you can map functions is, so the,
the default for that would be a function that given the model returns sub.none. So when you,
when you start out your builder, you have that. And then if somebody says with subscription,
then they pass in a function that takes a model and returns the subscription. And when they add
that, then you take the existing subscription in your app builder type and you say given a model.
So like, you know, backslash model arrow app builder dot subscriptions model. So you take
the existing subscriptions that you have on your app builder. You pass that the model,
and then you do sub dot batch with that to combine it with the new one that you've added. So in that
way, it's kind of an advanced concept, but my point is that you can sort of write out the function to
say to declare that function again, and then pass the argument into the existing function,
if that makes sense. So you can sort of like, keep applying something to a function and accumulating
values. And it like, it really hurts your brain, but I just want to put that out there. I don't
know who needs to hear that. But for the person who does, hopefully I saved you some time.
In this case, just use a list of functions, but I understood the concept.
And that could work too. Yeah. Yeah, cool. Any, any parting words of wisdom about builder patterns?
No. I think we're done here.
I think I think we've given people a good deal to think about. Yeah. All right. Well,
go play with builders and and let let your room know that you demand a phantom types blog post
from him and and then we'll loop back around and we'll do a podcast episode about phantom types.
We'll do a podcast episode about phantom builders. Yeah, let's do that. So if you have a topic that
you would like us to discuss or a question that you would like us to answer, you can go to
elmeradio.com elm dash radio and then go to submit a question and submit your question.
Ask away. We love getting questions. Yeah. Tell us your name, how to pronounce it if you want,
if you don't want us to butcher it. Maybe we'll get to that question if if we think we can answer
it. Well, yes, we're especially interested in talking about tools and techniques. So if your
topic can be about that, that would be great. Yes, absolutely. All right. Well, we'll eagerly
await your questions and until next time, take care. You're in. Take care.