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