spotifyovercastrssapple-podcasts

The Builder Pattern

We discuss the tradeoffs of using the builder pattern and how to get started with it.
July 13, 2020
#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

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.