The Life of a File

We revisit Evan's classic talk and dive into the process for how and when to split out Elm code into modules.
October 5, 2020

Core mechanics of Elm modules

  1. (Organize) Grouping functions values types
  2. (Hide) You can hide some of those things. Allows encapsulation, shielding from breaking changes, avoiding coupling.

Giant update functions

What are you gaining from extracting a module?

  • Protecting invariants

  • Hiding internals

  • Decoupling

  • TDD helps drive module design.

  • Experiment, but review your experiments before they become deeply ingrained.

  • Pain in code is for sending a message.

  • Technical debt isn't about "clean code". It's abstractions that serve what the code is doing. Abstractions are inherently expensive and a type of tech debt if they don't serve a purpose for your specific needs.

  • Be proactive - immediately as soon as there is a clear way to make code better (not perfect, small improvement) - do it

  • Relentless, Tiny Habits

  • elm-test Elm Radio episode

  • Testing is helpful for identifying modules - see keystone testing habit blog post

  • Property based testing is a sign that something is a module - it has a clear property, which means you want to protect the internals

  • It's okay to get it wrong, just don't get it all wrong up front with premature abstractions.


Hello, Jeroen.
Hello, Dillon.
How are you doing today?
I'm doing pretty well. How are you?
Pretty good. Pretty good.
And well, on we continue with our book club.
What are we talking about this week?
So this week, we're going to talk about another very special and very important talk from the Elm Street
community, one made by Evan this time, so Evan Cieplicki. And it's the life of a fowl.
Yeah, there really were some amazing talks that I think seeded a lot of great ideas in the Elm
community early on, you know, that Richard and Evan gave as keynotes in the early years.
I'm really grateful for those great resources that you've given us.
Because I think they've really had an impact on the way people think in the community.
Yeah, I think they shaped it.
Yeah. Yeah. And I think that was pretty intentional, too. And they did a great job with that.
Yeah. Richard and Evan, if you ever want to do those kind of talks again, please, please do.
We would love to hear those.
Well, you still get a lot of interesting talks coming out that Richard gives in various forums.
It's, you know, not as much these days, but.
Yeah, they're very interesting, just in different ways.
Yes. Yes. So what is this talk about in a nutshell, the life of a fowl? What's the short summary?
The short summary I would say is, well, Evan says it is how do I grow Elm code?
Yeah. Which is a big question.
Which is a big question. So what he says in the beginning of the talk is when people started using Elm a long time ago,
they were asking, OK, how do I write Elm code? How do I write Elm applications?
So Evan made all those utilities and functions that are now browser application and so on,
which gives you a structure to create Elm applications. And after that, people ask, how do I grow Elm code?
And this is what we're going to talk about now. And I feel like this talk answers that question.
And now the question is more like, how do I make an SPA? It is the one that I often see in the beginners channel of Slack.
Right. And to answer that question, you can go back to you can listen to episode 12 that we had about Elm SPA.
That's a start. Yeah. Although there's so much more you can talk about on that subject, too.
Yeah. I still feel like that question hasn't been fully answered.
Right. Because there are several ways to do it.
And even Ryan who writes Elm SPA is still making some changes in his designs.
Yeah. Although Richard Feldman's front end masters courses are quite good at going into those details
and how to structure a single page app and techniques for working with one.
So that is a good resource there. But there's more to be said on that topic, for sure.
So I think to me, this talk, the life of a file, the thesis is sort of this is how people tend to approach growing JavaScript into lots of tiny files.
And why do they do that? Because of spooky effects at a distance and because it's very painful to refactor.
So they sort of prematurely create these abstractions because it's such a pain if you don't have the right structure from the start.
So you start so you don't have to refactor because refactoring is scary in JavaScript.
So refactoring is not so scary in Elm and you don't have these spooky effects at a distance in Elm.
So what are the implications of that for how you contrast growing JavaScript code to growing Elm code?
And that's sort of Evan's central thesis, I think, is the way you grow code is more sort of holding off until you actually feel the pain of something.
And then you start to expand out to different modules. You experiment with your data modeling, too.
That's another thing you talked about. He talked about exploring all the different ways you can model something, which in our make impossible states impossible talk episode,
we didn't really talk about that process of playing around with a lot of different ways to model something.
And I think that's really good insight, too, because it's not just about impossible states.
It's about exploring and trying a lot of different things.
So I think he captures that in this talk, this exploration and this sort of process of waiting until you actually feel the pain instead of creating abstractions because you think you'll need them.
Now, there are a lot of details to get into there, but I think that that's the core thesis as I see it. Does that resonate with you?
Yeah, that resonates for me. So in JavaScript, you tend to like smaller files because you don't like spooky distance.
What spooky action such as this is all of that expression. Yeah, me too. I only hear it often.
That was wasn't that the reason that Einstein didn't like quantum mechanics or something that he.
I think it was because he he didn't believe that God was playing with dice.
Mm hmm. Yeah, that's the quotes. So maybe Einstein wouldn't have liked JavaScript.
Yeah, yeah. If you try something twice, you shouldn't you should expect the same thing.
Yeah, that's right. Exactly. It would have loved pure function programming.
I think so. So, yeah, you don't have those spooky actions from a distance because you don't have mutations.
Yes. And you don't need to cut off your lines that is a certain number of lines.
Like you don't need to have all your files be smaller than 300 lines long or even your functions be smaller than some right number.
Right. Yes. That's that's a really good takeaway is like, how do you know that code is hard to understand and change?
It's not necessarily the line count. There are other things to look for, but it's not necessarily that.
Yeah, I think in Elm review, I have a file that is almost 6000 lines of code now.
Mm hmm. And it's fine. It's I do want to split it up and I can't because of some thing I can't say.
Exploiter going to think. Yeah. Basically a compiler bug or limitation.
And yeah, even with 6000 lines of code, I'm I'm like, this is fine. Yeah, I can live with this. Right.
As soon as I can, when I'll do a break and change, then I will split it up like again. This is fine for now.
Yeah, even when you think of splitting by a number of lines of code, there is absolutely no metric that says whether it's OK to cut it 300 or 1000 or 2000 or 50 lines of JavaScript developers.
They they want their files to be small, but there is no yeah, no no metric to use. Like, when is it fine or what is the magic number?
Like, no. Right. So I think it's really nice that in Elm we can leave that arbitrary decision or arbitrary number metric and and focus on splitting by other other metrics.
Yeah, right. Yeah, I think that, you know, I often think about we want we want loose coupling and high cohesion.
That's you know, that's how you want your code to be. And what happens sometimes in languages, you know, like JavaScript and Ruby,
sometimes I see code being split in a way where it's actually highly coupled and not cohesive.
So you have like these two different files that depend on each other in these intimate ways where they're like, you know, directly sharing data, touching the same global state or whatever, you know.
So that makes them very coupled. So you can't really think about one without thinking about the other.
But then they're in separate places, so you don't have any cohesion. And that's that's the opposite of what you want.
You want, you know, I often think about it as like localized reasoning. That's like this, you know, sort of headline feature of like pure functional programming, you know, that you can look at one area of code and think about it.
And now sometimes that area of code might be like a part of a module of a very long file and you can understand it without pulling everything else into your head.
And that's fine. Sometimes you want to use modules to extract things out. And and there are reasons for that, too.
So so one of the things I think about for like a module, what are the things that a module does?
There are there are two things, literally only two things that a function does fundamentally.
Take inputs and give output. The way I frame it, the difference between having all of your code in a single module versus splitting your code into different modules.
The only difference is one, you're grouping some types and functions or top level values into a separate module.
That's one thing that's different. The second thing that's different is that you can hide some of those things.
Those are literally the two only things that an L module does. Now, those two things are very interesting.
The organization part is obviously useful. The hiding thing is a game changer because by hiding certain pieces,
you're able to have internals that other modules can't depend on and don't know about or care about.
Yeah. And you you avoid coupling between your data structures and your implementations.
Exactly. You cannot introduce breaking changes by changing those internals because the outside world doesn't care or know.
And you can you can think about it in a more high level way.
So I think that just understanding those core mechanics of a module are helpful for knowing when is it time to break out a module?
Because, well, do I want to group some things together and do I want to hide some of those things?
And, you know, Evan kind of talks about in this talk how there's there's something about, you know,
the way that you create a module and choose what to create a module around that it's based on a type.
There's something to that. Now, that's not always the case. You could have like a, you know, list helpers module, which is like, yeah,
you can have certain utilities that help with strings. And now that might be a special case because you're sort of extending what you can do with a particular type in a way that you don't have access to the internals.
But it's like a built in type or a type that is owned by another area of code.
So you can't, you know, sort of encapsulate those internals and then have access to them in your helpers.
If you could, then you might. But there's something to like, I mean, ultimately, you want to create these abstractions around domain concepts.
And I think that that's the core. And you want to create types around domain concepts, too.
When you say domain concept, do you mean data structures?
I mean data structures and like a concept that like you would talk about to a user, for example.
You know, oftentimes you want to be able to use language that, you know, you would describe some, you know.
Like this is not a string. This is a username. Yeah. This is not a string. This is a user ID.
Exactly. Exactly. It's a user ID. It's a, you know, whatever your fruit basket that you can put fruit into.
And like how does a fruit basket work? You can only have two fruits at most at a time, you know, which is the example from Life of a File.
If you haven't watched the talk and you're wondering why I'm talking about fruit baskets.
I should buy stock for I heard they've been working well these last few days.
Right. Yeah. So like that's something that you could talk to a customer about.
Right. And so how do you understand like what are the rules for your fruit basket or, you know, Evan doesn't call it a fruit basket, but you know what I'm talking about.
How you could talk to a user about that. You could explain those rules to the user that, oh, you can only have two things at once in your fruit basket.
And so you can now have a module that's responsible for that idea and encapsulates that.
And I think that that's an essential part of how you design modules. So like something that comes up often when people are getting into Elm or when they're really like growing past a certain point in their Elm code bases.
They talk about having giant update functions. What are your thoughts on that, Jeroen?
I would really have to look at the update function to know whether it's something that is OK or whether it's not OK.
I guess it's more about how many different concepts, how many different things does the update handle?
And can you delegate those tasks, those concepts to other functions or to other modules?
Right, right. Yes, I really like the word delegate there. I think I like to think of the update function as a delegator.
That's that's its job. You know, it's like it's like a telephone operator that's like moving the wires between connecting calls.
But it's not part of those conversations. It's just routing those calls to the right place.
Yeah, it could handle those calls inside the inside each pattern, or it could just call another function to do the job for him.
Right, right. Ideally, if your function is really big, you probably want to call separate things into different functions, which you might put into a different modules or not.
Right. And I think that it's a really good sign when you're when logic in your update function is moving out of the update function and into domain modules.
So like in Life of a File Evans, you know, fruit example, he was moving that logic from the update function to just directly calling that module in the update.
And that's all the updated. It called this module to add or remove an item.
Yeah, I think that's a good sign. And that's something to look for if you're trying to, quote unquote, clean up your update function.
Right. Because it's not that's the thing. It's not about cleaning up the code.
It's about being able to think at one level of responsibility and one level of abstraction.
So that like as you go down the update function, it should be operating at one level of abstraction, which is routing.
That's the responsibility of the update function. And by routing, I don't mean page routing, but I mean delegating a function to the right place.
This is a life of a founder, the life of an SBA.
Right. Yes. So I think it might be tempting to sort of, you know, I mean, Evan talked about having like update helpers or, you know, an update module or something like that.
Right. And when you do that, you might be splitting out the code into smaller pieces.
But that's not the goal. The goal is to decouple the code.
The goal is to make it so you can think about one area of code without loading up all of the rest of the code into your head.
That's the goal. You don't want to have to think about how one thing works when you're dealing with something entirely different.
Right. Yeah. I mean, sometimes I feel like it would make sense to even try being like very pedantic about the update function and saying, like, for example, you can't directly update the model in the update function.
You have to do it by calling a function. Yeah, sure.
I think that would be a useful experiment. I think I think that that's the right spirit.
And I always try to move towards that. I've never tried pedantically just like enforcing that.
That could be like an Elm review rule, actually. That would be interesting.
Yeah, let's try that at some point. That would be cool.
Maybe we can do a code coda on a live stream sometime and we can maintain some code and have that rule that we can never from the inception of the logic, it has to live in a separate function.
Yeah. And also no unused functions and one step at a time with your...
What was it called again? TCR. Test, commit, revert. Oh, it's so fun. It's so fun. Maybe maybe an episode on that sometime. It's a fun one.
Yeah. So one way I would usually look at separating things into different modules is by looking at the model and seeing what is connected, what is coupled, what field is coupled to another field.
Like with remote data, if you're trying to, if you have a record saying is loading, which is a Boolean and data, which is maybe something, then those two are coupled together.
You can't have is loading true and you have data. That is probably not what you're expecting.
So those I try to move into a separate type.
Yeah. And what so that's the first step that I would take and then you will likely have some functions to work with those.
And when that becomes a problem, you want to move those into a different module or so that you don't get tied to the implementation details.
So you don't get coupling. You can preemptively move that one out to a different module so that you can hide the implementation details and don't and avoid getting any coupling.
Yes. This is one of the the biggest things on my mind when I was rewatching this talk is this question of when is the right time to extract a module?
And I think, you know, Evan's advice on that question in this talk is basically don't do it prematurely, which I completely agree that premature abstractions are a real problem and people have a tendency to do them.
And I think that you need to hone that habit to to build up that sensibility.
That said, once once you have a sense of what what's natural and fits into a particular module, I actually I think that it's actually a good idea in in certain cases to start with a module rather than organically feeling the pain and moving it out.
Now, so like Richard talked talked about this in scaling out in make data structures.
Which we haven't covered yet. We covered, yeah, we haven't covered scaling applications, but we covered we covered make data structures.
And in that talk, he talks about that word count bug where the word count would get out of sync because there were different sources of truth.
And when he encapsulated that as internal details so he could have a certain small area that's responsible for making sure the word count is accurate and up to date and it could cache it as it wanted to and recalculated and whatever the bugs went away.
Right. Yeah. And he said, I wish I had started with that abstraction.
I think that's an example where I would actually recommend starting with a module just like, okay, we've got this chapter and we know it's got a word count.
And I don't want to let everybody depend on these internals of the word count.
I think it's reasonable to start with that and say, I have this invariant.
I want to protect this. Now, that said, if you're if you're new to this process and you don't have a lot of experience sort of organically extracting out modules.
I do think it's good to sort of practice doing that. And because there are going to be times when you're going to be tempted to extract things out and it's actually okay to leave it where it is.
Yeah. But once you once you develop those skills, I actually recommend in cases like that, starting with a module. I think that that's actually a good idea.
Yeah, I think it would be useful to know about starting, but when you're trying to extract things to at least try to gain some usefulness about it.
So when I met with my example of extracting the remote data pattern, which again, what you get is you decouple your code, which is which is a benefit,
which is good and you gain guarantees, which is good. But if you just arbitrarily take a function from one file to another file, you have to think about what do I gain?
So, for instance, if you have a utility function and you say, I'm going to move it to a utils module.
Right. Right. And it's even though it's never used anywhere else.
What do you gain? You don't gain any guarantees. You don't get remove coupling. Yes. Yeah.
You don't get any bug safety around that. So there's not much point to doing that.
Yes. Once you have something else that will call that function, maybe, maybe, maybe it will be useful.
Maybe it will be detrimental to reuse it, as Evan showed that not everything that looks the same is the same thing.
Totally agree. Yeah. No, that's well said. I think you're absolutely right that there are many cases where you're going to have utilities or little helper functions that it's going to be leading you astray if you try to abstract that into a module.
And as you said, you know, what are you gaining from it? Are you protecting invariants? Are you hiding internals? Are you removing coupling?
And if you're not doing any of those things, then there's no reason to extract something to a module.
So I completely agree with that. I think that there are times when, you know, I want to protect this, these internals.
I want to have an invariant or I want to make sure that this is used correctly. You know, I have like a Social Security number.
I think I'm going to abstract this right away into a module, for example.
Yeah, because as you said, with experience, you know that, oh, this is going to need to be protected.
This is just going to be a string under the hood. So I need to wrap it up and protect it.
Yes. So when you get some experience, we will know this will need protection.
Yeah. So this will benefit from having a type and this will benefit from being split into a different module.
Right. So you have the benefit there. You just know it.
Exactly. Exactly. You need to be driving those decisions based on those concrete benefits.
Exactly. I completely agree. So one other technique that really helps you stay honest about getting real benefits from this is test driven development.
If you're writing tests for something, you know, if you say like, oh, I wish that I don't know, I've got this shopping cart and I, you know,
if I add two items, then it should change the count of the shopping cart items rather than adding a duplicate entry or, you know, whatever.
That's something that you probably want to test drive. You, you know, write a unit test first.
And well, what are you going to test drive? Your main dot elm file, you know, your.
Yeah. Like you're going to test drive a module. Actually, I often like to start with testing code that's directly in a test file.
So it's just a function in the test file. And then I extract that function to a module once I've sort of gotten it up and running testing because that's the smallest step I can do to get a passing meaningful test.
But that said, I think that, yeah, I mean, what are you going to test drive? Right. You're test driving a module.
You could test drive little utility functions. And but even in those cases, like where do those utility functions live?
You need to find a home for them that you're going to test. You're probably utils.
Utils, man. Utils. You probably have like this mirrored structure between your, you know, a module and a test.
There's always there's almost always a one to one mapping of those things for unit level tests, not necessarily for, you know, end to end or integration tests, not necessarily for program tests.
One end to end test file per module. Yeah. That's that's I don't really want to imagine that.
Integration test. Why not? Maybe. So another thing that talks about right at the beginning of the talk is in JavaScript, he says, or at least in other languages, you need to get the architecture right from the start or you're doomed.
Yes. And that is something that we do not feel in the Elm community because refactoring is easy.
And for bigger projects, I feel like that is true or truer than in other languages, but it's still quite a lot of work.
Like if you have an architecture that is deeply rooted in the application, like you have a hundred thousand lines of code and there's one architecture that rules it all.
Yeah, it will be very hard to refactor or it's not hard. It's tedious. It's long. It's going to be a work incentive.
I like to think and to advise people to try it or whatever they want to.
Like, hey, do you think this is a good idea? Is it is it a good idea to talk to my father, my project this way?
And I usually tell them I would do it this way, but you know what? Try your try what you had in mind.
And if it doesn't work out, you can just refactor it. The only thing that I would say then is you need to be very critical of how your experiments is working.
I need to worry about that before it gets way too spread out. So before it gets the cornerstone of your whole application and where it would become still easy to change, but just very long and it would take you days or months of work.
Right. Yes. Yes. That's a really good point.
I think, you know, the other book club episodes we've been doing have been talking about these sort of more fixed qualities of code.
You know, what what do you look for as code? What do you move towards?
And the life of the file is more about the process and what do those tips look like and how do you get there and how does it evolve?
And, you know, one of the things that I think is really important here, you know, is that as you're saying, that process requires you to be continually reflecting on what are the pain points?
What are what are the problems we're moving towards?
If the architecture is going in in a direction that's going to hit a brick wall, you need to be paying attention to that.
And, you know, so I mean, Evan talks about avoiding premature abstractions, and I completely agree with that.
I think the way I think about refactoring is that you want refactorings to be the pain you feel when you're working with code is like a gift.
It's this gift that is a clear understanding of what you need to do because the pain is telling you something.
Right. I mean, that's what that's what pain is in our nervous system, too.
Right. If you have pain in your leg, you're like, hmm, I'd better look down at my leg because I think I need to change something like, oh, I'm stepping on a Lego.
OK, I'll lift my foot up and take the Lego off.
Like the pain is sending you a message of something you should do.
And then it turns out it was just pain in your elbow and you need acupuncture and that was a spooky action at a distance.
Spooky action at a distance. Yeah, there you go.
So, well, you definitely yeah, if you've got your code base arranged a certain way, you could definitely have pain signals coming from from places besides the origin.
But yeah, so those signals are are something you need to listen to.
Yeah. And if you don't make design decisions and refactorings based on those signals, then what you're doing is you're actually creating a type of technical debt.
People think of technical debt sometimes as like the code isn't clean enough.
The code isn't refactored. That's not what technical debt means.
Technical debt means that the abstractions or lack of abstractions are not serving what the code is trying to do.
So it's dependent on what the code is trying to achieve.
And if you anticipate what the code is going to do, then you're going to create abstractions that don't serve what the code needs to do.
They're not useful abstractions. Abstractions are not inherently good.
Abstractions are inherently bad, but that cost is offset by the value they provide.
But if they're not providing value, then abstractions are a type of technical debt.
So, yeah. And if you abstract prematurely, then you're probably not having the proper abstraction.
Yes, therefore, technical debts. Exactly.
You're creating a type of abstraction which is serving an imagined anticipated need, not an actual concrete need.
And so you're likely to be creating the likely process is you create an abstraction because you think you're going to have this need.
You think your code is going to need to be generalized in this way.
You later find out your code needs to be generalized in this other subtly different way.
That abstraction doesn't help at all. You have to unwind that abstraction.
You have to introduce another abstraction to solve that other problem.
So that's what you want to avoid. Right. So the process is that you you pay attention to the pain.
But when you do have a clear signal that like, OK, I now know that we need to do this thing that our code is not well designed to do right now.
That's when you do something. That's when you take action.
If you don't do that, if you're not in the habit of noticing that pain, thinking about what it means and taking immediate action in a small step,
then your code base ends up, like you said, getting you into trouble where you hit these walls and now you've got to do a one month refactoring.
So when when you say or when Evan says don't refactor, don't abstract prematurely.
Often that will mean write something twice or make this look keep this looking ugly until you know that something should be abstracted one way or another.
So if you do that at some point, you will have that ugly code, that non refactored code, maybe duplicate a code.
Yes. But you will. But once you find that abstraction, then you should abstract it.
But you will have created technical debts because you should have abstracted.
But it's just something that you have to pay somehow.
So either you pay the technical debts upfront by doing the right abstraction or you do it after the fact by waiting.
But it's still cheaper than doing the wrong abstraction first.
Yeah. Yeah. I mean, one thing I think about is, you know, sometimes I see people like extracting functions before before it's used multiple times.
But before it's used multiple times, how do you know what the function needs to look like?
What how do you know what it is? How do you know what something is before it happens two or three times?
So I think that, you know, sometimes I will intentionally introduce duplication so that I can step back and say, hmm, what does this want to be?
What abstraction is this looking for with actual concrete examples that tell me that information?
Sometimes I sometimes I feel like people people are uncomfortable because the way I refactor is either too late for them or too soon for them.
Ironically, both both things happen.
You know, sometimes people are uncomfortable that like, shouldn't we clean this code up?
Like, well, I don't think it's a problem yet. Let's let's like let's figure out what this function should be called.
I'm not really sure yet. Let's give it a nonsense name because I don't usually pick food.
Yeah, I usually do something. That's my go to. Some people do applesauce.
I've heard applesauce because it makes it very obvious that, you know, you need to come back and clean it up.
But, you know, so sometimes I'll sort of let things sit on the back burner a little bit so I can come back to it when I have more clarity on on what abstraction is needed or what something should be called.
And sometimes that makes people feel uncomfortable. Like, shouldn't we clean this up?
Like, I'd like to let it sit there and feel the pain.
Sometimes I think I make people feel uncomfortable because I'm like, OK, let's refactor this and commit it and push it to production.
I what like but like we're working on this. I know. But like now I see a better name.
Now let's ship that and change it right now. And that's how that's how I like to work, because I think that it allows you to feel the pain to help you understand better what abstractions are needed.
And then it as soon as as soon as there's like a concrete thing that, you know, will serve your code well, an abstraction, moving something to a module, having a type that represents something better that, you know, like this is a step in the right direction.
This is better. You should do it right away. Anytime, you know, you can do that.
But you need to do it safely in a small step, you know, guided by unit tests, safe refactorings, using your ID tooling to automate the refactorings, you know, all these types of things.
And that we're probably going to cover in a different episode.
And I kind of, you know, this, this particular element of this process being being guided by habits, I actually just released a blog post about this, called relentless tiny habits.
And so we'll link to that if you if you want to read more about like, cultivating these habits in that process, you can check that out.
But I think habits are are an essential part of this and feel the pain, but then be proactive and it seems like a paradox, but I don't think it is.
I have a habit of creating modules when my modules get too long.
So you go by line count.
Obviously, not all habits have been created equal.
So so we talked about we talked about line count isn't necessarily the metric for what makes a module difficult to understand.
And what what gives you a signal that you need to extract something?
What are the metrics and the signs that code is hard to understand?
Well, we can't really say that line counts isn't part of that.
A module that is 50 lines long is way easier to understand one that is 10,000 lines long.
The length definitely makes a module hard, harder to read over easier to read.
Right, right. I guess I guess the question there is, like, does splitting that now make it easier to read?
And it doesn't actually so that so that's really the more interesting question, isn't it?
What at what point does splitting a module make it easier to read than it was all in one big module?
Yeah. It's more like how or when or for what reason?
Yes. And yeah. More more about how in this talk from Evan.
Mm hmm. Yeah. I usually go by coupling, as I said before, create types for things that are coupled together and extract that.
Yes. Right. Yeah. I like to think about responsibilities like what it what does this code know about what does it need to understand?
You know, I feel bad for it if I give code too many responsibilities, you know.
You write a lot of comments saying sorry. Yeah. So sorry.
It's like if you're you know, if you're at a friend's house for dinner and they're like cooking and cleaning and, you know,
and you're like, come on, let me do something like at least let me chop some vegetables or like clean some dishes for you.
You feel bad if they take on too many responsibilities. Yeah. Yeah, that's true.
So I feel bad for the code to like it, you know, give it a break, like let it focus on its job and then let someone else do another job.
Yeah, that's a good one. Yeah. I think we kind of hit on on this earlier that, you know, what what can you use a module to help you do?
So good, good reasons to want a module. You know, you want you want to test a distinct thing.
You want to hide an internal detail. Test a distinct thing on its own, not within an environment.
Yes, that's right. That's right. Exactly. You want the banana, not the whole jungle.
Right. I think that unit testing is I mean, it's a whole topic and we we went into it in depth in our own test episode.
But that approach is so helpful for identifying responsibilities because it makes it a lot more clear.
So I think that doing test driven development is going to make your job of identifying the right modules a lot easier.
But but yeah, I think, you know, you want to hide internals. You want to, you know, guarantee certain properties of your code.
Evan talked about, you know, fuzz testing, property based testing being in some ways like a sign that something belongs as a module that you're like,
well, if I can identify a property that I want to check about something that this always applies or this always applies in the in this type of case.
If you can write a fuzz test for it, then you can also use the module's ability to hide certain internal details to protect from the outside world.
Getting becoming responsible for this task of protecting that condition.
Yeah. If you can fuzz test a sub part of something, but not the whole thing, then that could be a sign that you need to split them out.
Right. Yeah. If you have like a, you know, URL that you say that if I combine together these paths, I shouldn't have two slashes in a row and I should, you know,
it should always end with a slash or whatever, you know, invariance you want to check about that.
That might be a good sign that that belongs in a module because you want to protect those details of how you deal with that data.
Yeah. So I feel like we haven't talked about one aspect from the talk is same versus similar, which is a big chunk of the talk.
So Evan takes the example of checkboxes.
Yep. So there's one list of checkboxes where you define settings for your user and one other list of checkboxes where you can choose fruits,
potentially only a limited number of fruits. And what he shows is that even though it looks the same, like you have a list of checkboxes in both cases,
you don't want to model them the same way. And because you don't want to model them the same way, you don't want to abstract them the same way because abstractions go around data structures in Elm.
And a big chunk of his talk is basically a cata of how would you structure these two use cases.
And it turns out you have plenty of ways to represent the data that is needed to display them.
And they all give you very different benefits.
Yeah. And sometimes you can't represent things one way.
For instance, you can't represent dynamic things that come from the server using a record.
You instead need a dictionary, for instance. He mentions that it's a good idea to try to think about all the use cases or all the possible data representations
and try to figure out the ones that bring you the most benefits that are closest to your use case.
And I think he does a very good cata for maybe 20 minutes, I don't know, around this. It's very educational.
Yeah, that's a good point. So what are the implications for same versus similar?
Like, what do you do with that knowledge? How does that change the way you approach things?
That makes me, I think, just not to refactor things or abstract things before I know what they will be used for.
Right. Right. Instead of saying like, oh, let me generalize this checkbox functionality.
I'll have like a checkboxes module.
Yeah. Or I have two dialogues here in my application.
I should probably abstract that into a single dialogue and with flags and stuff.
Yeah. That might be a good idea, but maybe you need to wait and see how they evolve.
Maybe that will make sense. Just have a dialogue layer, dialogue module, and have the internals be different and not one module behind flags.
Yeah. You have to think about all those abstraction, the cost of abstractions and what benefits they bring you.
Right. And like the different ways of slicing it and defining those responsibilities or domains.
Yeah. It's like Evan talks about just exploring a lot of different ways to model data, which I think is such a great lesson.
Yeah, definitely.
And I think that that applies to exploring how you can sort of abstract something and slice off a certain responsibility.
You know, like you said, is it like, like, what is the thing here?
Is it a dialogue or is it, you know, a user selection thing?
Yeah. Which happens to be a dialogue, but might not be tomorrow.
Right. Right. And maybe this other thing that's also a dialogue is so different that they're similar,
but at the core, the way that you model the data and all these things underlying them are different or maybe they are the same.
But you can like I think it's good to practice just slicing things a lot of different ways, like just write it down on paper.
Like, what are all the different ways you could choose the dividing lines?
You know, like this, this is one responsibility. This is one responsibility. Just write down lots of different ways.
Because if you only have the first thing that comes to mind as the only choice you have, you're really limiting yourself.
And I think that in this process, the best teacher is experience and experience that comes through actually experimenting and feeling the pain.
And if you don't allow yourself to feel the pain of not having an abstraction, you're not going to learn those things.
But if you do, sometimes you'll go to a dead end.
Sometimes you won't, but you'll be paying attention to, you know, I don't have an abstraction here. OK, why is that painful?
What's painful about it? And that's telling you something about the responsibility.
And you need to listen to that. Think about different ways you could define those responsibilities and try it.
And that's how you learn. So we talk about a lot about coupling in this episode.
That is actually not very much part of my vocabulary. I don't think about coupling.
What I think about is making impossible states impossible.
So whenever I see a data structure where the fields can be in a state that is invalid, that's where I think about making impossible states impossible and moving them out to a new type.
I never think about the word coupling, but I think essentially that is pretty much it right.
Or also getting stuff in sync.
Yeah. Keeping data in sync or having the variance, the relationships defined appropriately so that their states are coupled in the right way.
There's a coupling of states, of data states.
Yeah. If this field needs to change every time that field needs to be changed, that is probably that makes sense to have a data structure or not be represented, but just recomputed every time.
That's interesting. I definitely think that the things you're talking about are a sign of coupling.
I'm not sure I would go so far as to say that that's the only thing that is a sign of coupling.
No, no, definitely not. But that is what I have in mind.
That might just be because I'm not all that used to using the word coupling in my everyday job.
It's a very abstract word. I actually do use that word a lot, but I think maybe a better word for it is just responsibility.
How many responsibilities does the same thing have? It's a lot more natural to think of it in terms of responsibilities.
You could have a conversation with a child and talk about, what do you think this is responsible for?
What's this responsible for? And they could have a brilliant conversation with you about it.
And you don't need to explain any concepts to them, which is a sign that it's a good word because you can arrive at good abstractions by thinking about this simple idea and coupling.
It's like, what does that even mean?
I don't even know how you say coupling in French. I don't know how to say responsibility.
Interesting. So how should people get started with applying these ideas of the life of a file to their everyday coding?
First of all, I feel like I always say this, but just watch the talk or read the article.
Can't go wrong with that.
Yeah, it's not even watch it, watch it, then rewatch it, then rewatch it again and again.
Take some notes, try some of the techniques, get some inspiration.
Yeah. Try to feel the pain. Just practice not abstracting too early.
And when you feel the pain, when you feel problems, then refactor and move things into types, into data structures, and then into modules.
And then when you have more experience, when you can tell that something will benefit being into its own module without it being premature abstraction, do that.
And yeah, make impossible states impossible. If you haven't listened to that episode, go listen to that one.
Yeah, there's overlap between all these ideas, which is, you know...
Build data structures.
Right. Organize things around data structures. And how do you choose your data structures?
Well, you make impossible states impossible. You choose the ones that have the properties you want.
Yeah, so all of these ideas sort of are in orbit around these core concepts.
I really feel like splitting around data structures or maybe even responsibilities is just the way to do it in Elm.
It's not like we've been brainwashed, although maybe. Evan is a good speaker, I think.
But it's more like we tried it and it worked. And we tried it again and it just always works.
It works. I mean, I don't think it's specific to Elm at all. I think it's inherent in just the idea of a module.
In Elixir, there are these like struct types that you can actually define a struct for a particular module.
So it's like it's almost like an object in a way that it's like this is the data type associated with this module.
And it's like built into the language.
OK, yeah.
And so that's definitely the way people structure things in the Elixir world as well.
And just like when you think about, again, the mechanics of a module, it can group certain types, values, functions, and it can hide some of them.
Right. So, well, if you what if you want to hide internal details, but it's defined in another module?
Well, that way of organizing doesn't work because you can't hide it, because now the things that use it don't have access to it.
So, of course, this makes sense that that a module is organized around a data type because, yeah, I mean, it abstracts the details of how you consume that data type and nothing else has access to those internals.
So just by that core mechanic, that's that's that's what you have to do to leverage that mechanic.
So I think, yeah, I mean, I think that it's a process. And, you know, as you said, Jeroen, experiment, try out the process, practice.
Try the wrong solutions.
Try. Yeah, exactly.
Try the wrong solutions so that you can feel the pain and then you can go and explain to your colleagues like this is not the way to go because this, that.
Mm hmm. Yes, it's it's an iterative process and it requires experimentation and you're going to you're going to get it wrong.
But what you want to do is you want to not get it all wrong up front.
If you do everything up front, then you're going to get everything wrong up front.
But if you take tiny steps as you feel the pain, I mean, if you just think about it like your time is limited.
And so what do you want to spend time on? Some thing that you imagine you're going to need an abstraction for in the future or something you need an abstraction for right now.
Like if you have a limited amount of time, work on the thing that you know you need now first.
Don't waste your time with other stuff. So, yeah, practice, practice deferring.
I know that I very deliberately practiced deferring, introducing abstractions when I was getting started with them,
because I think maybe even because I had heard this advice from Evan's talk, I can't remember.
I can't remember where I got this inspiration, but I remember I was I was holding off on it because I didn't want my object oriented background to guide my design decisions.
And I actually was able to avoid a lot of the pitfalls that like Richard talked about in make data structures
where he had to learn the hard way that, you know, creating these abstractions around or maybe he talks about this in scaling applications.
But he talks about this, you know, creating abstractions where you're trying to encapsulate data and you're trying to use this object oriented approach where your message passing between all these different data types.
And I actually didn't need to feel that pain because I was disciplined about deferring abstractions and letting myself feel the pain and be open to a new set of like design ideas.
And that really helped. So I definitely recommend trying that out.
That said, when you know that something can have a better name, when you know that something can be extracted into a nice function that groups this stuff together, then do it right away.
All right. Well, I think we've wrapped the subject now.
I think we I think we covered. Well, that's that's everything there is to know about designing on code.
We've covered it all. This was a fun podcast. Thanks for listening.
Yeah. Now, you know, impossible states, you know how to how to incrementally build files.
We're done. Yeah. But I still don't know how to to make SPS.
There you go. So that'll be the rest of the podcast.
Talking about that. But if you do have something you'd like to hear about on the podcast, send us a question or a topic suggestion. Go to Elm dash radio dot com slash question questions.
So many questions. Yeah. You can you can find the button there.
Yeah. And review us on Apple podcast. We'd appreciate a review. And thank you so much for listening. And you're in.
I'll talk to you next time. See you next time. Bye bye.