Check out a free preview of the full Advanced Elm course
The "Using Modules for Modularity" Lesson is part of the full, Advanced Elm course featured in this preview video. Here's what you'd learn in this lesson:
Richard discusses the benefits of organizing modules around a single type, and the cost of splitting modules based in file length rather than based on types.
Transcript from the "Using Modules for Modularity" Lesson
[00:00:00]
>> Richard Feldman: And the final thing is using modules for modularity. So what does that mean? One of the things we noted about the editor file, it's pretty long. It's about 600 lines of code. That sounds like a lot for one module, or for one file. At the risk of repeating something that Evan, I think gave an excellent talk on, which we'll link to called the Life of a File.
[00:00:22]
One of the things worth noting about Elm Files is that the primary purpose of a module is this line right here, exposing. It's to hide things, it's to organize things that are related into a particular module, and then to say some of these implementation details only matter to the related things inside this module.
[00:00:43]
And generally speaking, they're built around a particular type. For example, on pages usually that type is the model. Everything in this page has to do with this model, and probably nobody else should be messing with that model. As it happens, due to this current, the way that this single page application is constructed, I do need to expose the model so that main can have access to it.
[00:01:04]
But that is hopefully a temporary implementation detail and in the future we can do away with that. A better example would be something like profile. Here's our profile. We have profile being an opaque type. It's got an internals record that stores some information that only this profile module cares about.
[00:01:22]
It doesn't expose them. So they're named internals for a reason, they don't go outside this module. We've got the opaque type of profile, then we have a very small API of stuff you can do with the profile. You can get the avatar, you can get the bio, and you can get a decoder for it, and that's it.
[00:01:39]
So this is a very compact Elm module. In terms of lines of code, yeah okay, it's also a lot smaller. It's only 50 compared to editor. Less than, more than an order of magnitude difference between the two. But to me, the bigger deal has more to do with how these types tend to relate.
[00:01:56]
When I was talking about debugging, talking about making changes to code, talking about making additions to code even. All I'm really thinking about is how these types interrelate and what things I can put out of my mind, and these pathways I can close off as I'm sort of traversing my search space for implications of what I'm about to do.
[00:02:14]
And what controls types? What controls the number of pathways that I have to traverse? What controls how deep those pathways go? Is what gets exposed. So just by virtue of the fact that this internals thing is not exposed, I know that any time here that I'm using internals, that's something that only exists in this module.
[00:02:34]
There is no external dependency to that. I know that for a fact cuz it doesn't ever escape this module. Likewise, even if I have a much bigger one like for example, author. Author is about half the size of the editor page, but is significantly bigger than profile. It's still built around this idea of one data structure, in this case the author.
[00:02:57]
So we have a decoder, like we did for the profile. We have a couple of other things. You can fetch an author, you can follow, you can get a followButton. There are requests for following and unfollowing, and then there's username and there's view. Those are the, sort of the, this is the entire API for working with an author.
[00:03:14]
And all of these things are related. And for as much stuff as this is exposing compared to the profile, there's a whole bunch more that it's not exposing. For example, it doesn't expose the constructors of these things. These are both opaque types. The implementation of username is completely secret from the outside world.
[00:03:30]
As far as outsiders know, username just says hey, give me an author and I'll give you back the username. And it says yeah, okay, that's fine, but I happen to know how to do that for all of the different types of things that go on within a username.
[00:03:44]
We also can get the profile out of those, no matter which combination of things we have there. So all of these things that are internal just like request help right here. We talked about help functions more in the next section. Anytime I have potential bug in any of these, anytime I have potential change I want to make any of these, all I have to look at is what actually gets exposed.
[00:04:03]
If it's not expose, if it's only a dependency if things that don't get expose then we don't have to consider any of the other modules. So really, that's where the sort of implications of one value on another, one function on another come from. It has to do with the types and what's exposed, not so much the length of the module.
[00:04:21]
If I were to take this editor page and chop it up arbitrarily, I just said, I'm gonna take the first third and move that into a module. And the second third and move that into another module. And the third third and move that into a third module. What would I have done?
[00:04:34]
Well, I can produce the line count. Have I actually depends on what else? No, I have the same search space before. In fact, now I only have a larger source space because I only have more files to traverse. And so, it's actually going to take me a longer to debug, longer to make changes, longer to figure out where things are going, because I'm switching files more often for no corresponding benefit.
[00:04:54]
I've already organized these things into a module such that I can draw tight module boundaries around it. And say, I can expose as little as possible because all of these things are related, they all go in one module, and they're all interdependent anyway. If I were to split them up into three different modules, I would start having to over-expose.
[00:05:14]
I would have to say this module needs to expose to the entire world this particular piece of implementation details just so this first module can have access to it. Whereas if I put them all in one module, if they only depend on one another, I can just not expose them at all.
[00:05:28]
And I can limit the scope to say this only is a concern within this module. Now, again, this only happens because all functions in Elm are pure. If I were in JavaScript that would not be true anymore. In JavaScript every single line in a module can potentially affect every single other line, every line of code.
[00:05:46]
You can run a side effect, you can mutate some global, it can read from a global. So there's no guarantee that by taking what's going on in a module, I can't just hop from type definition to type definition, and think only in terms of the types to figure out how things interrelate.
[00:06:00]
I also have to look at all the lines in between to figure out what's going on. So that creates, naturally speaking, a habit to say, lower line counts are better. Lower line counts are more maintainable. And I think in JavaScript that's true. But it's not true in Elm the opposite is true.
[00:06:16]
When you think in terms of line counts in Elm, you're sacrificing this huge opportunity to draw tighter boundaries. When you split things up arbitrarily based on line length, you're forced to over expose and actually cause more harm than good. So the better structure when you're scaling an Elm application is to start off by just picking a module and adding to it.
[00:06:37]
Just saying, I've already got this module here or maybe I'm creating a new page. New pages say you're gonna want a new module for it. And just start adding stuff to it. Just build, build, build, build, build. And at some points you'll decide to split it. Not because you said it was too big or too many lines of code.
[00:06:52]
That's not really a thing. But rather because you've identified a subset of it that you could pull out and put it into its own module, because they're all related. And you could hide parts of it. So the motivation for splitting out a module is not so much that we've decided something was too big or too many lines of code.
[00:07:10]
But rather that we've identified the opportunity to make something else that's smaller. Out of some related group of functionality. That's really where the money is, because that's what feeds into all the things we were talking about previously. All these ideas of how we're going to traverse our code base when we're trying to figure out the implications of change, a bug we're trying to fix, or even just some new addition we're making.
[00:07:31]
So, that's what I mean by using modules for modularity. To break things up into self contained modules that expose ideally less than what they implement. Let's move on to the Review. So what fits in our heads? This is the idea of eventually our program is going to outgrow what we can keep in our heads if we keep building, it and building it out.
[00:07:52]
The only solution to that is to be able to have techniques for chopping it up into smaller bits. So that we can eventually narrow down the potential implications of what we're trying to do to something that does fit in our heads. Or if its still something that does fit in our heads at least something where we can make a change, and not the compile tells what to do next.
[00:08:10]
Our mean tool for doing this is narrowing types. If we were to say we're gonna pass model to everything and how everything will turn a tool of model and command a message we really couldn't narrow this down at all. We would have to look at every function, everything is a potential corporate for every bug.
[00:08:24]
Every change that we make potentially has an impact on everything else. But if we narrow the types, we can very, very quickly rule out potential culprits and potential things that are impacted by changes we're gonna make just by looking at the types. We don't even have to glance at the implementation.
[00:08:38]
We talked about enforcement arguments, which is a way of sort of, instead of narrowing types, it's a way of adding arguments to something in order to show a dependency that's actually there in terms of our business logic. So something, for example like saying, we don't want to make is possible to render a follow button or a favorite button unless you're logged in.
[00:08:55]
Or even the save button, because you're only allowed to do those actions if you are logged in. By putting them in the message, we say this is no longer possible to do in the view. You can not instantiate one of these unless you're able to provide an argument that demonstrates that you are logged in.
[00:09:09]
We talked about using modules for modularity which is to say the idea of growing your models one at a time. Building them out, building them out, building them out. And only splitting them into different modules when we realize that there is a subset of them that we could profitably split out, and make something smaller, a self contain unit, usually based around one particular type.
[00:09:28]
As oppose to saying this file feels too long, I'm going to split it, which can cause the problems of needing to over expose things in order to make the dependencies work out.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops