spotifyovercastrssapple-podcasts

Scaling Elm Apps

The techniques for managing state as an application grows look very different in Elm than they do in Object-Oriented and component-based frameworks. We discuss the differences and best practices.
December 14, 2020
#19

Transcript

[00:00:00]
Hello, Jeroen.
[00:00:02]
Hello, Dillon.
[00:00:03]
How are you doing today?
[00:00:04]
I'm doing very well.
[00:00:05]
How about you?
[00:00:06]
I'm doing well.
[00:00:07]
And what are we talking about today?
[00:00:09]
Today, we will talk about scaling Elm apps.
[00:00:12]
Yeah, this sort of turned into another little book club talk, didn't it?
[00:00:16]
Yeah, we didn't expect it to be like this, but I think we'll do it.
[00:00:21]
So Richard Fellmann has made an awesome talk called Scaling Elm Apps, Elm Europe 2017.
[00:00:28]
And we want to talk about some of the same subjects.
[00:00:33]
And it turns out we want to talk about the whole subjects.
[00:00:37]
Yeah, I think some of the things we'll be touching on, I think it'll end up being sort of a superset of that talk.
[00:00:44]
Well, hopefully we will bring something to the table.
[00:00:47]
Yeah, yeah.
[00:00:48]
So one of the big questions that comes up a lot in Elm applications as you start to scale is,
[00:00:55]
how do I encapsulate these pieces of state that I have into self contained units?
[00:01:03]
That's always the question that people want to use this familiar pattern of a component.
[00:01:08]
You said it.
[00:01:10]
You said it.
[00:01:11]
Coming from a background doing React or other object oriented paradigms,
[00:01:16]
people tend to want to reach for something that's a component.
[00:01:19]
And that seems like this natural way to split things.
[00:01:23]
And I think that a lot of people who have worked on larger scale Elm apps will have experienced some of the pain
[00:01:31]
of going too far to sort of componentize the way that you write your Elm code.
[00:01:36]
So that's going to be one of the things we dive into today.
[00:01:39]
So the big question for me is, how do you draw the dividing lines?
[00:01:46]
That is one of the most interesting questions about scaling Elm apps in my mind.
[00:01:50]
What are some of your thoughts about how to draw those dividing lines?
[00:01:54]
There are no dividing lines.
[00:01:55]
Just put everything in one giant main file.
[00:01:59]
And no problems, no parent child communication, no components, and everyone is happy.
[00:02:05]
Right.
[00:02:06]
Now, of course, we did an episode on growing.
[00:02:10]
I know what you're talking about.
[00:02:15]
Well, we did hundreds of episodes right now.
[00:02:18]
Oh, yeah. And of course, we did a whole episode on the life of a file, which was another book club episode.
[00:02:24]
And we sort of touched on this topic.
[00:02:27]
But I think there's still more to say about this in terms of a larger scale app that has to do communication.
[00:02:34]
So that's sort of a different question.
[00:02:36]
Or perhaps it's really the same question, but it deserves some revisiting.
[00:02:41]
So one of the key takeaways from scaling Elm apps, Richard's Elm Europe talk, is narrowing the types.
[00:02:50]
Yeah. But for a reason, to keep less things in our minds and make the things that we work with smaller.
[00:02:59]
Yes.
[00:03:00]
And easier to work with.
[00:03:01]
Exactly. And if you draw those lines a certain way, and actually Richard illustrates it really nicely with this image of the sort of farmers market.
[00:03:10]
Where he splits some tomatoes and some different vegetables into different regions versus drawing the line and getting all these different seemingly unrelated things in the picture.
[00:03:22]
And so I think that really illustrates well how you can separate things out in a way that they aren't cohesive.
[00:03:31]
So like a classic example of this to me is having like a decoders.elm module and putting all your decoders in there.
[00:03:40]
Yeah. My example would be splitting by main model view types subscriptions, which is also I think something that, yeah, you're drawing the line somewhere, but it's not at the right place.
[00:03:53]
We already talked about this with the life of Elm.
[00:03:57]
That's right. That is how you scale Elm apps.
[00:03:59]
But you know that that might be the answer, but that doesn't really give you much about what to do about that.
[00:04:05]
And so, so, okay, you've got, you've got something that has a decoder and encapsulates a data type.
[00:04:11]
It's built around a data type.
[00:04:13]
We talked about all those things in our life of a file episode.
[00:04:16]
And Evan talks about those things in his talk, the same title.
[00:04:20]
And that's great.
[00:04:21]
But then what about when you have to actually handle events and send messages?
[00:04:25]
And how do you slice things when that's the case?
[00:04:29]
And so this sort of component based mindset would say that you have a self contained state.
[00:04:35]
And essentially I would describe this component based mindset in Elm, which is generally not recommended.
[00:04:42]
I would define that as trying to wire through a mini Elm architecture for every self contained piece.
[00:04:51]
So you draw those rectangles throughout your page and then you sort of take each of those rectangles and you say,
[00:04:59]
okay, well this is like a Russian doll nesting where we've got each of those contained state,
[00:05:05]
which might contain other bits of self contained state.
[00:05:08]
So those, that's the sort of component architecture.
[00:05:10]
Yeah. Which works in React, for instance.
[00:05:12]
It works very well in React because I mean, you can, so let's talk about that.
[00:05:18]
Like why does that work well in React and not in Elm?
[00:05:21]
I would say it's because you have access to things like global states.
[00:05:26]
So in Elm we work a lot with the single source of truth, which you can't access from any component.
[00:05:33]
You need to pass it as an argument to different functions.
[00:05:37]
So you can't access it in a read way and you can't also write it.
[00:05:41]
But in React you can do that.
[00:05:43]
Yeah. And you can also just sort of send events and respond to events and...
[00:05:48]
And do side effects.
[00:05:50]
Right.
[00:05:51]
Yeah. Like running to the global model if you have one, is one such effect.
[00:05:57]
Right. You can mutate things globally if you need to in a particular instance.
[00:06:03]
And it's not necessarily going to require you to sort of wire up sending messages back to the parent.
[00:06:10]
So that can be a more natural way to do it in that paradigm.
[00:06:14]
So now I will say there are times when it is very nice to sort of encapsulate that local state.
[00:06:20]
You know, if you've got some sort of, you know, if you want to have a date picker or some sort of sophisticated UI component
[00:06:29]
that has a lot of internal state and you just want to sort of know, tell me when you've, when you're done picking a date.
[00:06:36]
Just let me know.
[00:06:37]
And you don't want to wire everything into the model and you know, you've got this in all these different pages and it's a pain to wire it in.
[00:06:44]
There's an answer for that problem.
[00:06:46]
You can use a web component and you could even, you know, you could even build that web component in Elm.
[00:06:51]
And that can be a really good answer in cases like that.
[00:06:54]
And I think that's one of the challenges of Elm is figuring out how do we split apart our code but not have this sort of...
[00:07:04]
Like we want the explicitness and all the benefits of pure functions and explicitness and all of that and no magic.
[00:07:12]
But we also want the convenience of being able to sprinkle something in and not have to manually wire it up and have verbose code that wires things through.
[00:07:23]
And it's like, well, you've got to pick one.
[00:07:25]
Although using a web component actually kind of allows you to do both where you have this self contained thing that has its state.
[00:07:32]
It has an interface, you know, through sending events like actual HTML events that you can listen for in your Elm code that registers it.
[00:07:42]
That can be a really good answer for those types of sort of state heavy UI components.
[00:07:48]
Yeah. People usually don't like that because then they have to write JavaScript.
[00:07:52]
But you can also write it in Elm.
[00:07:54]
Yeah. You do lose all the niceness because you need to work with insurgency and communication through ports or attributes.
[00:08:04]
But there's a middle ground.
[00:08:05]
Yeah.
[00:08:06]
And JavaScript is fine too, honestly, if it's not too big.
[00:08:09]
Right. If it's like a UI component or perhaps it's an off the shelf UI component that you're using.
[00:08:15]
I mean, with something like, you know, material UI, you know, you've got these like ripple effects when you hover over or click on buttons and things like that.
[00:08:23]
And wiring that into an Elm app is really a pain.
[00:08:26]
And doing that kind of thing, web components are exactly suited to solving that problem in a framework agnostic way.
[00:08:33]
So, OK, so when it comes to actually, you know, there are certain cases when you need to communicate state from one part of the code and you need to send a message up.
[00:08:43]
You know, a button gets clicked, some input gets changed, a validation happens, something comes back from the server and you need to sort of trigger it from these different parts of your app.
[00:08:52]
And so another principle that I think about here is composing things.
[00:08:58]
And sometimes I like to think in terms of a hook.
[00:09:02]
I kind of like the term hook for, you know, something that gives you a point that you can hook into that you say, when this happens, let me know.
[00:09:12]
And I want to do these other things. It's like a, you know, it's like a push notification or a code subscription, like even the subscription in HTML.
[00:09:21]
Yeah, exactly. And so I think like one thing that I see happening sometimes is that you get this code that just piles on these responsibilities.
[00:09:31]
And that's the thing to look out for. You know, I think that like scaling applications, it's all about, you know, just trying to find those dividing lines and how to organize things.
[00:09:41]
And if you see all these different responsibilities being mixed together, you know, I mean, we've talked about this. There's an art to seeing those responsibilities.
[00:09:49]
If you're looking at JSON decoders, you know, you might be tempted to say, like, oh, that's one responsibility.
[00:09:55]
This is your responsible for all the JSON decoding. You're responsible for all the view code.
[00:10:00]
But then you lose your ability to to do nice things like, you know, opaque types.
[00:10:07]
You have you have to jump between code in all these different places.
[00:10:12]
I think one good heuristic is when you sit down to work on a task, how many different modules do you have to jump around to?
[00:10:19]
Yeah, usually you just want to work in the module that you need to change and maybe the one that uses it or the ones.
[00:10:27]
But not different modules that are linked to each other and that are coupled to each other.
[00:10:34]
Yes. Yeah. Yeah. I mean, if you're if you're changing one thing, then ideally you want to change things in one place.
[00:10:42]
You want to. But now if you're changing two things, then if you have to go to two different modules, then that might be a good sign.
[00:10:49]
And, you know, I mean, it's much easier said than done.
[00:10:53]
But I think it's very easy to pile on responsibilities in a single module and have it be a sort of hub for all these responsibilities that get grouped together.
[00:11:03]
And it can turn into a sort of junk drawer of, oh, this happens and these these events need to occur.
[00:11:09]
And, you know, I need to change the state from this step to this step.
[00:11:14]
And then when that happens, it needs to have these validations and it needs to store that between pages.
[00:11:20]
And suddenly you've got all these different responsibilities in one module.
[00:11:24]
But if you can instead compose those together, you know, in object oriented programming, sometimes people talk about prefer composition over inheritance.
[00:11:32]
And there's a reason for that, because when you compose things together, you can think about them independently.
[00:11:39]
When you inherit, it just intermeshes them and intermingles them in a way where it becomes very difficult to think about them separately.
[00:11:47]
And because they're not separate simply. Right.
[00:11:51]
You've coupled them together. And with composition, you can really think about things independently and say, OK, these things, both of these things need to happen.
[00:11:59]
But I can think about them separately and put them together sort of at the edges, not at the center of the system.
[00:12:06]
Yeah. So maybe let's go back to how to divide simple things.
[00:12:11]
So I was half joking when I said put everything in your main file, because that kind of works.
[00:12:17]
And you you don't have a lot of problems around what some people call parent child communication.
[00:12:23]
Everything is one module and everything is pretty easy.
[00:12:26]
It's not maintainable because your your update and your view do way too many things, but it's relatively easy.
[00:12:35]
So if you want to draw the dividing lines, make things simpler.
[00:12:39]
Often what you will do is just try and make bigger functions into smaller functions.
[00:12:43]
So Rich's example was I have a view function, separate that into a view header, view body and view footer.
[00:12:52]
And those are just simple view functions. So you don't have to make them separate.
[00:12:58]
So all you're doing is just moving code. You're not adding any wiring at all except calling the function.
[00:13:04]
And that is very easy refactor and very easy divide.
[00:13:08]
Yeah, right. Those are like the low hanging fruit wins.
[00:13:11]
And in general, I think it's a very good idea to start with low hanging fruit, too, because you just clear out all these things and suddenly you're like, OK,
[00:13:20]
I know this is this is an easy win. This header, I can I can think about it on its own.
[00:13:26]
Let's put it in a module. And then suddenly you come back and look at the module that was starting to get unwieldy and you can see it with more clarity.
[00:13:34]
So starting with those obvious wins is a really good place to start.
[00:13:38]
Yeah, you can do that for any functions, any view function, any update function, any decoder function, helper function, anything.
[00:13:45]
And that's where we go back to you, to what you said about composition.
[00:13:49]
Composing functions is pretty easy, but composing components is surprisingly because of the name is complicated.
[00:13:59]
Yeah, right. Complex components composition.
[00:14:05]
It's a mouthful. It's a good tongue twister.
[00:14:08]
Come, come, come. Yeah, no, that's not going to be able not going to say that again, I think.
[00:14:14]
Yeah. Even harder to do it than it is to say it.
[00:14:17]
And I think the next step is then to to move things into a new module.
[00:14:22]
As we said with the life of file, you have something that deals with one concept.
[00:14:26]
Move that to a separate file that handles that one concept, that one responsibility.
[00:14:31]
And when you're dealing with the architecture with with view and all that, that is what people usually call a component.
[00:14:40]
And sometimes we call that a model view update triplets.
[00:14:44]
And this is where things become a bit harder because often you will need to create a new message type.
[00:14:51]
Yeah. And then you need to either use HTML dot map to fit those messages into the bigger picture or compose those messages.
[00:15:01]
Richard calls it the teach me how to message pattern to pass in a function for creating the message and then compose those together with with pipes.
[00:15:10]
Yeah. This one simpler alternative is you reuse the same kind of message as main has or the parent has because of an import cycle.
[00:15:19]
You would need to move that to a message module.
[00:15:22]
And that's where people start splitting everything by module view and update.
[00:15:26]
And that simplifies in one way, but probably not in the right way.
[00:15:31]
Yes. Right. So let's talk about that. So I let's talk about the core mechanics at play here a little bit.
[00:15:39]
What are the foundational forces at play?
[00:15:42]
You know, like we've got our our gravity and our electromagnetism and all these different you know, what are these forces?
[00:15:51]
And I think I think one of the key forces which Richard talks about in scaling L maps is narrowing down what something can do.
[00:16:00]
So when when by default you're making something as broad as possible, then it's harder to reason about it.
[00:16:07]
Right. That's that's like a basic operating principle that I think is is an intuition you should listen to for guiding you towards how you how you design your own code and evolve the design.
[00:16:18]
So Richard talks about if something needs to return a command, you know, or a message, it needs to do that.
[00:16:25]
And there's there's nothing you can do to prevent that unless you reduce the scope of your actual application, which right.
[00:16:34]
Now, sometimes that does happen. I wrote an article.
[00:16:37]
I'll share the link for this sort of design process that I went through for graph QL at one point when when I started to build map to into a selection set.
[00:16:47]
And the challenge was that when you when you map two selection sets together.
[00:16:52]
So if you're selecting two different fields, you can actually you know, we talked about this a little in our graph QL episode.
[00:17:00]
You can select two fields with the same name, but different arguments.
[00:17:04]
You could select an avatar that has a large size argument and one that has a small size argument.
[00:17:10]
And in GraphQL, those need an alias to prevent a collision there.
[00:17:15]
And so the you know, the older version of Elm GraphQL was doing this approach where you build up a selection set, you say all the fields and it sort of needs them all at once.
[00:17:25]
You can't arbitrarily add selection, add fields to a selection set.
[00:17:30]
You have to add it in one place because it would then add an alias to each of those fields.
[00:17:36]
But then if you throw something else in, it doesn't it would need this extra state to know how to add a unique alias.
[00:17:44]
If it's adding avatar one and avatar two and then you map another thing in there with map two that already has an avatar.
[00:17:52]
Now it needs to track the state of all the aliases it's used.
[00:17:56]
And so there's an inherently inherent complexity there.
[00:18:00]
And so, you know, in a nutshell, the design insight that I had for solving that problem was that I could uniquely create an alias for a field based on all of these different things that go into that.
[00:18:15]
The arguments that it sends, the name of the field, and the it also depends on the type of the field.
[00:18:22]
So I had to use that information.
[00:18:24]
But you take all those things together, you create a unique cache.
[00:18:27]
And now I don't have to think about that.
[00:18:29]
There's no opportunity for fields to collide.
[00:18:32]
And I don't need that state in a selection set to make sure they're unique because they're unique by default.
[00:18:37]
So that is something that happens.
[00:18:40]
And the name of that article, I think, was how Elm guides towards simplicity.
[00:18:44]
So that actually is something that can happen, that you realize a simpler way to design something at a sort of architecture level, like at the core of the problem.
[00:18:54]
Or sometimes you rethink the way that you're building a feature because you realize there's an inherent complexity that guides you towards a different approach that might be just as useful for users but easier to build.
[00:19:05]
So I don't want to actually discount that because sometimes that's a perfectly valid thing to do, to make something simpler, and Elm reveals the inherent complexity.
[00:19:15]
Elm isn't a silver bullet that's going to make dealing with complexity easy, but it reveals your complexity to you.
[00:19:22]
And that's the power of it is you can see what's complex and you can see what's not.
[00:19:28]
So all that said, if you can make something inherently simpler, great.
[00:19:32]
But assuming that you're not going to change the actual problem at hand, if something is, you know, needing to communicate some global state, you click this one, you know, button and then it changes something globally, it closes a modal or whatever it may be.
[00:19:49]
That's an inherent complexity that you need to have.
[00:19:52]
But if you don't have that, then don't make the lowest common denominator communicating and making all possible information available.
[00:20:01]
And so pass in the minimal amount of information you can and interact back to the parent minimally as well.
[00:20:09]
Richard talks about that idea of sort of narrowing the types and the effect that that has when you're thinking locally.
[00:20:16]
I mean, it's all about being able to think locally and, you know, putting your detective hat on when you're trying to understand a bug or understand how to add a new feature.
[00:20:26]
And being able to eliminate options as irrelevant to your investigation.
[00:20:31]
That's that's what it's all about. So that's like one of the kind of core mechanics of improving your your application design.
[00:20:39]
I'll try and explicit what you meant with eliminating possibilities.
[00:20:42]
Like Richard gives the example of I'm looking for a bug somewhere, some HTTP request was made and it shouldn't have. I'm looking for where that could have been created so I can stop it from being created.
[00:20:57]
And then you go through all the functions and you if you want the HTTP request, you need something that returns a command.
[00:21:03]
So you go through all the functions and you find ones that return command and the ones that don't you can just ignore.
[00:21:10]
Exactly. Yeah. Yeah. So you're narrowing down those paths and your search space narrows.
[00:21:15]
And I mean, that's the to me, that's the really incredible thing about Elm.
[00:21:20]
And so that's I think one of the trade offs with some some approach like, you know, react that, you know, react is appealing in some ways.
[00:21:28]
And a lot of people really love it. The thing that I love about Elm is that, you know, sure, maybe it's harder to just sprinkle in a little bit of JavaScript magic somewhere that, you know,
[00:21:39]
you get this super powerful extra functionality and you have an Apollo caching layer for your GraphQL automatically and you send a mutation and it just automatically stores cached requests.
[00:21:53]
But then when that's not working, where do you look? Everywhere. Everywhere.
[00:21:59]
And, you know, not to mention the dynamism of JavaScript as a language compared to Elm.
[00:22:05]
So that so it just Elm is incredible at narrowing down your search space and being able to think locally.
[00:22:11]
And I think that's so beautiful. But but you have to structure your application in a way that leverages that.
[00:22:17]
And if you structure your application where you have these many Elm architecture patterns, these triplets that return model command and out messages all over your app,
[00:22:28]
you've thrown that benefit out the window. Effectively, it's what's happened.
[00:22:32]
Most modules expose the triplet, the model, the view, the update, sometimes the subscriptions.
[00:22:39]
And they often have the same signature like model is function that takes a message, a model and returns a model and a tuple of model and command message or just returns a model.
[00:22:52]
But it can return a lot of things. And that is some flexibility that is very, very useful.
[00:22:59]
And we have to keep in mind that we can add arguments and we can return more things that than what it's custom to do.
[00:23:07]
That and that is where boilerplate comes in because we have to write all that boilerplate.
[00:23:12]
Well, we can change that boilerplate however we want.
[00:23:16]
And that is one of the strengths of having that sometimes annoying boilerplate that a lot of people complain about.
[00:23:21]
Yeah, I think a nice way to think about this is, you know, I've heard the term used. It's actually from this book, The Goose Book, Growing Object Oriented Software Guided by Tests.
[00:23:34]
I think that's the title. And of course, it's talking about object oriented design.
[00:23:39]
But they also talk about having a functional core and they talk about outside in testing.
[00:23:44]
And it's sort of a well known book for sort of introducing this outside in testing style where you start with the end to end tests and mock things in the middle.
[00:23:52]
I'm not sure I tend to be more in the sort of I guess the Detroit school Kent Beck sort of inside out testing school of thought there.
[00:24:02]
But I really like this idea that they have of talking about a matchmaker and having this be a responsibility that a matchmaker is delegating.
[00:24:12]
It's I often think about like a telephone operator that just a phone call comes in and the operator.
[00:24:19]
The operator's job is not to resolve the issue and fix the problem.
[00:24:24]
The operator's job is to route it to the right person. That's all their job is.
[00:24:28]
Hey, I need an auto mechanic. OK, let me patch you in. Hey, I need can you. I'm trying to call this person. OK, I'll patch you in.
[00:24:35]
That's their only job. That's their only responsibility.
[00:24:38]
You know, we kind of talked about how decoding is not a good way to define a responsibility.
[00:24:43]
That's just something that with experience you learn that, hey, you know, decoding isn't actually there's responsibility.
[00:24:50]
That's like one of the one of the things that an article needs to do. An article needs to decode.
[00:24:56]
It also has a data type. It also knows how to fetch an article from the server.
[00:25:01]
Those are the response. That's the responsibility is sort of everything involved in that data type of an article.
[00:25:07]
Well, another thing that I've learned from experiences, I think that a matchmaker is a good responsibility and it's a good thing to think about that.
[00:25:16]
Hey, if this is doing too much logic, maybe the job of this thing is really to be a matchmaker and to delegate to the right places.
[00:25:23]
So, you know, your update function, if you see a lot of logic in your update function, that's a great opportunity to pull things out into some functions and to extract those out into some modules.
[00:25:34]
Usually around some sort of data type. And, you know, this is where you sort of try to narrow that interface.
[00:25:41]
So if something cleanly pulls out of your of your sort of page component.
[00:25:46]
Now, pages are a place where you want to have this sort of triplet that you can communicate, you know, view and update to perform commands and stuff.
[00:25:55]
I think that components work really nicely at the top level.
[00:25:59]
There's siblings, these page components in Elm, but they don't work nicely in this sort of nesting hierarchy from the top down.
[00:26:07]
You have this component which contains this component, which contains this component.
[00:26:12]
I would say they work well when you really know what is possible and what can happen.
[00:26:19]
And that is the case at the top level with pages.
[00:26:21]
So you've got frameworks like Elm S.P.A. which creates page components because we know what can happen.
[00:26:29]
But even creating that structure limits what it can do.
[00:26:33]
Yes. And Elm S.P.A. actually is a really good example of this narrowing technique because even though under the hood, every page is a component, which is fully the same, that it has access to the shared state and it has access to its own local state.
[00:26:49]
And it has an init and it has an update.
[00:26:51]
You can narrow it by using these sort of simpler constructors, which we talked about in our Elm S.P.A. episode that you can say, hey, this is going to be a static component.
[00:27:03]
It's just a view. It doesn't have any update or anything.
[00:27:06]
And now that narrows your search space if you're trying to understand an issue or trying to understand how that page works.
[00:27:13]
But so, yeah, this idea of a matchmaker, I think, is very handy.
[00:27:19]
If something is delegating to a lot of different places, then it shouldn't be too smart for its own good.
[00:27:25]
And if it is, then that's an opportunity to sort of split pieces out so it can just delegate.
[00:27:32]
That should be its job.
[00:27:33]
Another thing I see happening sometimes is having these sort of layers of abstraction for these delegators.
[00:27:40]
So having like a, you know, you've got your page component for the main page, but then you've got some sort of abstraction around handling some part of the state.
[00:27:51]
And it's got its own messages and updates that's just delegating, but it's not really doing anything itself.
[00:27:58]
It's sort of delegating.
[00:28:00]
I'm not sure I follow. Can you give me an example?
[00:28:04]
So I've seen an example with a sort of page wizard state that that page wizard state has this sort of this flow where these, you know, it's changing to different steps and it's sort of delegating.
[00:28:17]
But it's sort of taking this responsibility for like part of the state and it's watering it down.
[00:28:23]
And you really don't want that delegation to go through too many different layers.
[00:28:27]
So it's really this sort of composition versus inheritance sort of thing.
[00:28:33]
Like you want to compose together.
[00:28:35]
Like when you delegate, you want to be delegating at the sort of top level of a page.
[00:28:40]
If you're delegating within these layers of abstraction, then what happens is it becomes really hard to trace where messages are coming from.
[00:28:48]
You know, have you ever seen this in an application where you're just like, OK, wait a minute.
[00:28:52]
So this message came from here, but where did that come from and how is that getting triggered?
[00:28:57]
And so you want those sort of connections to be going through as few layers of indirection as possible.
[00:29:04]
You want it to be more flat in that regard.
[00:29:07]
And composition helps you do things in a flatter way.
[00:29:10]
You also want it to be familiar.
[00:29:13]
Like we have the Elm architecture where we usually have those functions that have the same signatures.
[00:29:18]
And when they do, that's great because they're familiar to us and we can change them.
[00:29:24]
If possible, it's nice to avoid that.
[00:29:27]
Right. I think that there's a part of us as programmers that we see all these different, this asymmetry of all these different function signatures.
[00:29:36]
Returning these different things. This one's returning a model.
[00:29:39]
This one's returning a model command tuple.
[00:29:43]
This one is just returning an email address.
[00:29:46]
This one's returning a result email address with validation errors.
[00:29:50]
We're like, this doesn't feel right. You know, it'd be really nice.
[00:29:52]
What if these were all uniform? What if they all just returned a model command message?
[00:29:57]
Wouldn't that be great?
[00:29:58]
Well, yep.
[00:30:01]
Turns out it might not be that great.
[00:30:03]
So this like desire to have uniformity sometimes serves us well.
[00:30:08]
But in this particular case, you have to keep in mind this dynamic of what does that do in terms of the cost of local reasoning and being able to eliminate possibilities.
[00:30:19]
So there's this trade off between like wiring because it's like it.
[00:30:23]
So, OK, if you if you make everything a sort of, you know, model command message componentized sort of or make it a triplet or a little mini architecture, right?
[00:30:34]
If you make everything have access to everything and being able to be able to change everything and perform commands.
[00:30:41]
I mean, now you've you've got JavaScript. You can perform a commit.
[00:30:45]
You can perform side effects.
[00:30:47]
All of you know, of course, it's it's still not not exactly the same as JavaScript, but you don't have runtime exceptions.
[00:30:53]
So that's an improvement.
[00:30:55]
Right. Yeah. But you've got this sort of there's this there's this tension between, hey, if I want to change this in the future, I'm going to have to do this extra work to wire that in and change that.
[00:31:07]
And we want to avoid that.
[00:31:09]
But what we also want to avoid is diluting what something is doing and making it less clear what something can change, what something depends on.
[00:31:20]
And so that's I think that's the big key advice is that instinct should override your instincts and your desire to want to have a uniform way of things interacting and not ever have to change the way things are wired.
[00:31:34]
If you have like those records or if you have like components which where you specify how to edit, how to update, how to view, they all need to work together.
[00:31:46]
They all need to be the same, pretty much.
[00:31:48]
So if you have a very small and simple component which only does have an internal state but not trigger commands, you still have to specify that you don't want any commands.
[00:31:59]
And you still need to accept arguments that other components might want to have.
[00:32:04]
So you have a lot of complexity and you have nothing that is narrowed down.
[00:32:08]
And yeah, that makes it very hard to find what something can do and what is ignored.
[00:32:13]
But if you if you make everything custom and just try to make it look the same by having names in its updates and subscriptions and so on, well, then it's familiar.
[00:32:25]
But you're not tied to one implementation where if you need to add a new argument, you need to add it to all the components because you've inherited this complexity.
[00:32:36]
Right. Now, that said, sometimes it can be nice to have these patterns of having like a session, like a sort of something that represents a global state that can be shared and passed around and persists between page changes.
[00:32:52]
But, you know, so that's a common pattern. And, you know, I mean, there's a way to sort of find a happy medium where you're not wiring through everything by default, but you can also have this sort of container for, you know, a lot of the sort of core things that are being reused all over the place.
[00:33:08]
Yeah. And I think that this makes sense because you will need the session in like most places in the application.
[00:33:15]
So you end up ignoring one argument sometimes.
[00:33:18]
Right.
[00:33:19]
And in some places you will just not pass it around and where where that argument is ignored, things will be simpler. So, yeah, try to ignore what you can when you can and make the simplest solution.
[00:33:31]
What's your opinion on extensible records, Jeroen? I think, you know, you've done a lot with this sort of like extensible functionality with phantom types.
[00:33:41]
Oh, yeah.
[00:33:42]
And that's a very interesting way to use that sort of part of the compiler. But what do you think about extensible records for data types?
[00:33:50]
I really don't like it. I've seen, I've only seen it used at my current company. I didn't before because I wrote most of the code and didn't think of that.
[00:33:59]
And it couples you to do some things that you don't want to do or that you don't need to do.
[00:34:04]
So an extensible record is type alias records where you have some fields and then some unknown fields and then you specify what those unknown fields are wherever you will use it.
[00:34:14]
Right. So you could have like a point type alias point equals record curly braces.
[00:34:21]
And instead of just saying X colon, you know, number and Y colon number, you could say in front of those fields that you declare, you could give a type variable name like point, lowercase p point, and then a vertical bar.
[00:34:37]
Sometimes I think about it as it's like this record is some sort of point record such that there is an X which is a number and a Y that's a number.
[00:34:47]
But now if you pass a 3D point that has a Z as well, it would have access to the X and Y because it knows that it has those.
[00:34:56]
But it wouldn't have access to the Z and it could ignore that. But you could pass a 2D point or a 3D point interchangeably.
[00:35:02]
So it's sort of like a type of polymorphism in Elm.
[00:35:05]
Yeah, kind of. I read it as points with at least X number and Y is a number.
[00:35:11]
So those are great for arguments as Richard and apparently Evan says, according to Richard.
[00:35:19]
Yeah, I wrote down Evan's quote that Richard quoted in this talk.
[00:35:24]
Extensible records are useful to restrict arguments, not for data modeling.
[00:35:29]
Yeah, so when they're used in arguments, you say my function colon takes a extensible record. So curly braces, points, vertical slash, X, blah, blah, blah.
[00:35:43]
That says this function will take something that has those fields and do something with it.
[00:35:50]
But that's just another way of saying it takes one argument X and one argument Y.
[00:35:56]
But just as a record and it could be a more complex record, but we don't care about it.
[00:36:02]
And that is just syntactic sugar in a way.
[00:36:07]
So if you model it as one argument with an extensible record, it's no different from a non extensible record or several arguments.
[00:36:17]
It's just it looks different, but it's in practice the same.
[00:36:20]
But when you do modeling with that, so if you put like if you have a user which is an extensible record, which has a name, for instance, which has at least a name.
[00:36:30]
And then you use that as user, which also has an age, which is a number.
[00:36:37]
Well, then if you start working with that kind of type, you need to fetch users which always have names.
[00:36:45]
And if you have the more fields you have in there, the more likely it will be that it will not be used wherever it is needed, wherever you will use that user.
[00:36:55]
And that just means that you're adding unnecessary complexity because now you need to initialize those unused fields.
[00:37:03]
You need to fetch them. You need to create them in a way that is complex sometimes.
[00:37:08]
And yeah, it's just a lot of unnecessary pain.
[00:37:13]
I think it would be much better to just say to create a new type that happens to contain the names, the same fields with the same names and same types.
[00:37:24]
You also have to pass around the type variable everywhere, which can be a pain.
[00:37:28]
Yep.
[00:37:29]
I've used extensible records sometimes.
[00:37:32]
Like sometimes a URL is a record in Elm which has a path and a host and whatever, a protocol and these different data types and values.
[00:37:45]
And sometimes I only care about the path.
[00:37:48]
And so I'll make my URLs an extensible record which only takes the path.
[00:37:53]
And then if I'm testing it, I can pass in a record which only has path equals some path.
[00:37:59]
And I can use that in the test.
[00:38:01]
Now, I mean, because I don't want to create like maybe something requires a URL, but other things don't.
[00:38:09]
And I don't want to create fake data for the URL necessarily, but I just want to make it clear what certain pieces depend on.
[00:38:16]
Now, you could just take the path and directly grab that.
[00:38:21]
Yeah, that would be the same thing.
[00:38:22]
Right.
[00:38:23]
And so I guess the, you know, maybe the argument for an extensible record would be that if you wanted to depend on another piece of data, then you could reach for it.
[00:38:34]
So it could be a trade off where you get this benefit we're talking about where you can narrow and restrict types and see what you're working with more clearly,
[00:38:43]
while at the same time you are able to grab another piece of data if you want to.
[00:38:48]
That's the idea.
[00:38:50]
In practice, generally I hear, you know, experienced Elm developers complaining about the headache of using extensible records to do that more than saying that it's a pattern that they really like and want to use more.
[00:39:03]
So it seems that somehow in practice, it tends to be more of a headache to wire that up everywhere than it is, you know, than it merits.
[00:39:12]
Yeah, it kind of feels like more like inheritance rather than composition.
[00:39:18]
Yeah, right.
[00:39:20]
You could add a user field to your records and compose it that way.
[00:39:25]
Yeah.
[00:39:26]
Rather than using an extensible record and you wouldn't have the same problems.
[00:39:29]
Right.
[00:39:30]
Yeah, I mean, it also couples you in an annoying way where, you know, if you say, hey, I've got a session, but I only need the auth token for this page.
[00:39:41]
So I'm going to make the type that this page takes be an extensible record, which is only shares this one field in common with the global session type of the auth token.
[00:39:53]
And, you know, now you've got that.
[00:39:55]
But now if you rename auth token or change the type of that, every single place where you use those extensible records is going to be causing a compiler error and you have to go and fix that.
[00:40:06]
And it just feels like heavy handed in that way.
[00:40:09]
But they're great for functions.
[00:40:12]
Just not for function return types.
[00:40:15]
Yeah.
[00:40:16]
Yeah.
[00:40:17]
Oh, you're saying they're great as an argument to a function?
[00:40:19]
Extensible records?
[00:40:20]
Yeah.
[00:40:21]
Okay, so you do like using them as an input value.
[00:40:24]
Yeah, definitely.
[00:40:25]
Yeah.
[00:40:26]
Just not as a return value because that means that it's data.
[00:40:29]
Got it.
[00:40:30]
Yep.
[00:40:31]
So you can take in a record and change just one part of it and return that.
[00:40:36]
And if you depend on a particular field, you depend on, you know, you take a session if you wanted a function that performs authentication if needed and it takes a session, but it doesn't know about any of the fields in the session about the auth token.
[00:40:50]
And if it has an auth token, then maybe it checks if it's fresh and make some requests or it fetches it if it needs to.
[00:40:57]
And then it updates the auth token.
[00:40:59]
So it changes the session to update that one field.
[00:41:02]
You could have some type signatures using extensible records where you've got, you know, a session is the only field in that extensible record.
[00:41:09]
So, you know, it's the only thing it depends on and it's the only thing it returns that it can potentially change.
[00:41:14]
So there are cases where that can be convenient, but it's I, yeah, I don't think I have a strong enough opinion as to whether you should avoid them at all costs or use them at every opportunity at this point.
[00:41:27]
Yeah. Well, you'll play with it and then you'll make your own opinion because that's one of the nice things with Elm.
[00:41:33]
Like if he, it's easy to refactor things.
[00:41:36]
So go ahead and play with something, try your way and then make your opinion.
[00:41:41]
And for some reason we usually always come back to the same opinion as experienced vettelberg for some reason.
[00:41:48]
Yeah. Yeah. For most things at least.
[00:41:51]
It is interesting. Like, I wonder if you could use this pattern.
[00:41:54]
You know, we sort of talked about like wanting this uniformity where you always have the same arguments coming in and you always have this like mini Elm architecture.
[00:42:04]
I wonder if you could, I wonder how it would feel if you did that with extensible records.
[00:42:09]
So you say, Hey, if I want to wire in a new piece, although I guess the problem with that would be that the only way you can wire in a command if it's being returned is if you know it's there in the extensible type.
[00:42:22]
So you'd still need to wire it in after the extensible record guarantees that it will be present.
[00:42:28]
So it doesn't really help you much because you can't just like treat it as a maybe and if it's there, do something with it.
[00:42:34]
And also you won't be able to know as easily whether something will return a command.
[00:42:39]
You will need to look at the signature of the implementation.
[00:42:43]
And there you see that it will know it won't return a command somehow, but not at the call site.
[00:42:50]
Yeah. Right. That's a good point.
[00:42:51]
But yeah, so I guess the, I mean, one of the big overall lessons, I think, so we talked about like the core mechanics of narrowing, being able to think more locally, composing rather than deeply nesting things.
[00:43:04]
And I think one of the takeaways too, you know, we've talked about this in the past is this idea of like, what is it that makes code maintainable?
[00:43:12]
I mean, we maintain and read code much more often than we add new features and change things.
[00:43:20]
Add new features.
[00:43:22]
So if you can make it easy to trace where things are coming from, even if it's more verbose, even if it's more difficult to wire in a new thing, that equation just ends up, you know, if you were to weight it as an equation of how maintainable is this Elm code.
[00:43:39]
Right. And you have.
[00:43:40]
So like, let's make it an index like maintainability equals, you know, some constant times readability plus some constant times localized thinking ability plus some constant times the pain of wiring in.
[00:43:57]
If I change things and that constant next to the pain of wiring things into change things is going to be very small compared to the constant in front of localized reasoning.
[00:44:07]
So you want to optimize for that thing, even if it means it's going to be a pain with wiring things.
[00:44:12]
And it's like, is that really the thing that's making you move slowly and code in your cycle slowly?
[00:44:18]
Yeah, I think you're right, because there's not a lot of ways to wire things in an Elm.
[00:44:24]
Right. It's a pain and we really don't like wiring.
[00:44:27]
And so it feels like there's something wrong when we're wiring.
[00:44:31]
But is that really what slows you down?
[00:44:33]
And actually, in practice, it's not it like it feels sleek and it feels like we're really flowing when we don't have to wire things.
[00:44:42]
But the things that we really get stuck on and that really slow us down is like, where is this bug coming from?
[00:44:48]
Where do I change this thing?
[00:44:49]
If I if I touch this code, what other things are potentially going to break?
[00:44:54]
And if you can optimize the heck out of that, then you're going to be moving really fast.
[00:44:59]
That's actually what makes you move fast.
[00:45:01]
I know there's a few packages which try to make the wiring easier, especially for like update functions.
[00:45:09]
Right. Which take like applicative style updates.
[00:45:13]
Yeah. Like and do update and add commands.
[00:45:18]
Those are quite nice.
[00:45:20]
Yeah, I don't agree with that.
[00:45:21]
You're not a fan?
[00:45:22]
No, I feel like that's optimized the wrong thing because wiring is pretty simple to read.
[00:45:28]
And easy to write also.
[00:45:31]
But if you add a new vocabulary for creating your commands and your updates, that makes it more complex because it's less familiar.
[00:45:41]
But I got you.
[00:45:42]
You don't bring a lot of new things to the table.
[00:45:46]
I see. So you're essentially saying that it requires a sort of uniformity that as we're talking about optimizing for that uniformity makes you not optimize for the really important thing,
[00:45:57]
which is localized reasoning.
[00:45:59]
So you don't like those packages because it requires that uniformity?
[00:46:02]
No, because I don't think it impacts local wealth.
[00:46:05]
Yeah. It makes it harder to read and to understand what the update function does in the case of the update function because that's a lot less familiar.
[00:46:15]
And I could learn it, but I don't find the original thing complex.
[00:46:20]
Like returning model and a list of commands is not very complex to me.
[00:46:24]
So yeah, having a package for that just makes things more complex in my head.
[00:46:29]
Yeah, I mean, there are definitely things that you can add in a pipeline that, again, you want things to be composable.
[00:46:36]
There are ways to add things in a pipeline where you're actually making things not compose nicely, even though it feels nice because it's in a pipeline.
[00:46:45]
And I don't know. I think in certain simple cases it can be.
[00:46:50]
So like I've seen some nice patterns in codebases where you have like, you know, whatever you need to refresh this cache and then you need to do this and then you need to do all these different things in the update function.
[00:47:04]
And you'll sort of chain those in and compose them.
[00:47:07]
So it'll be a sort of like specific API for that code base to say, hey, these are the types of things, you know, make sure you're logged in and then do this thing or whatever it may be.
[00:47:16]
And that can be a quite nice way to compose things.
[00:47:20]
So and we've talked about this before, build your own tooling, sometimes like relying on packages for that.
[00:47:26]
Sometimes those things are better as inspiration rather than just using them off the shelf.
[00:47:30]
Just like LMSPA example, use it as an example, but don't reuse the code as it is to start your project with, I'd say.
[00:47:37]
Right. Yeah. Make your own guarantees. Make your own ways of composing things together.
[00:47:41]
All right. Well, what resources can we give people to help them get started with this journey of scaling their Elm applications?
[00:47:49]
I would be remiss if I didn't say Richard Feldman's scaling Elm apps.
[00:47:54]
Yeah. I think we've mentioned it before.
[00:47:57]
Well worth a watch. Yeah. He really...
[00:47:59]
Several watches.
[00:48:01]
Yeah. Yeah. There's a lot of good stuff in there.
[00:48:03]
And he does a really good job sort of giving a lot of concrete examples of this process of narrowing.
[00:48:11]
And so that's worth paying particular attention to.
[00:48:15]
I think just browsing the LMSPA example repo is a really good idea too.
[00:48:20]
If you haven't already spent some time digging into that and looking at it, it's well worth just reading through that code and the accompanying article.
[00:48:28]
I think you can reuse the same patterns for like parent child communication between quotes, which you can... Yeah.
[00:48:36]
I think that's it.
[00:48:38]
Yeah. All right. Well, let us know if you, dear listener, try any of these techniques for scaling your Elm applications or have any stories.
[00:48:48]
Tweet them at us, Elm Radio Podcasts, and drop us a review, iTunes Podcasts, and send us a question at elm.radio.com.
[00:48:57]
All right. Well, until next time.
[00:48:59]
See you next time.