All about the composition lens
In this episode, I introduce the composition lens, its questions, and its goal of figuring what's true when you perform multiple operations in a row.
[00:00:00] All about the composition lens.
[00:00:03] Hello, my name is Eric Normand, and this is my podcast. Welcome.
[00:00:09] I'm writing a book and it's all about executable specifications as an approach to domain modeling where you build the domain model in your implementation language, interactively, iteratively. A fun way generates a flexible and expressive model, and I'm organizing the material into what I'm calling lenses.
[00:00:42] Each lens provides a different perspective, different questions that it asks, different skills it develops to help you better design software. Software design is very complicated and it requires a lot of understanding of the context, and so these perspectives help you uncover that context so that you can make better design decisions.
[00:01:15] It's not rules of thumb. It's not trying to say, oh, if you do this, your software will be designed better. It's trying to get you to see the information that's there hidden so that you can make better decisions.
[00:01:31] And today I'm talking about one of the lenses, it's called composition. I was calling this algebra before, but I kind of feel like that was confusing people and I had to explain why I call it algebra, and I'm experimenting with calling it composition cuz that's the spirit of algebra and it's kind of the essence of what I want people to be thinking about. And so why confuse them with the term algebra? They either know what algebra is from high school and they'll be like, why are we doing that? Or they'll have a sense of algebra from college or or later math, mathematics courses, and be disappointed that I'm not going deeper or more formal with my algebra. So I'm just not gonna bring it up anymore.
[00:02:36] Composition. The question that we're asking here, the main question is, how do multiple operations compose together?
[00:02:48] So when you're writing software, you have some data type that you're operating on, rarely do you just do one operation on it. Usually you're doing multiple things in a row in some kind of structured way where the argument to one function is the return value of another function, and that's what I'm calling composition.
[00:03:21] It's not as strict as what is often called function composition, which is a very special way of threading the return value of one function into the argument of another. This is more informal. But in any case, it's composed together.
[00:03:41] You can't just take each operation by itself. One thing that we start to see when we get good at domain modeling is that the composition is really important. The composition is maybe most of the interesting stuff. That's where it happens is between operations and so this lens is asking us to question, to explore, what are the ways that things compose?
[00:04:21] Now, one way things can compose is with themselves. So you could have, a property that shows that the order of operations doesn't matter. We've talked about that before. That's called associativity. It's a very common property that you probably learned in algebra class. I have a whole episode on associativity. I don't want to go into it right now, but suffice it to say that when you write out the formula for the associative property, you see that you're composing the operation with itself. You're stating that composing it in this way is the same as composing it in that way. It's a very important and useful property. Go listen to that episode as I go deep into why it's useful.
[00:05:25] Another property where the operation composed with itself is idempotency. So when you have an idempotent operation, doing the operation twice is the same as doing it once. So you see that composing it with itself has no effect other than the first effect.
[00:05:51] Here's an example of the composition in our domain, in our coffee domain that we've been talking about. You might set the size, then set the roast, then change the size again. You're composing these set size, set roast, and set size with each other. Because the coffee that's returned by the first operation is then threaded to the second operation, and then its return value is threaded to the third operation.
[00:06:22] And you might wanna say some properties about this. You might want to say, Hey, setting the size and then setting it back to what it was, that's like a no op.
[00:06:36] That's an interesting property that holds. There's nothing about this coffee that has changed. Let's say it's large, I set it to small, then set it back to large, that should be the same coffee. It doesn't change the way the coffee is made that I set it and then unset it. Same with the roast. We're gonna have the same thing.
[00:07:03] Now, another composition that might get more a little bit more complicated is adding and removing add-ins. Okay, so two different operations technically adding and removing, but they compose with each other. And so we wanna see how does that work?
[00:07:22] What properties can we guarantee when we do that? Now, one you might want to say is we can add an add in and then remove that same add in, and that's a no op. And that sounds obvious, but it might not be true. Depending on how we implement it or how we define the semantics of it. Let's put it that way, cuz right now we're just talking about models. We're not talking about implementations of code.
[00:07:57] So if I put a limit, because the coffee shop has a limit, the coffee shop says you can only have five add-ins in a coffee. You know at some point you run out of room in the cup. There's no coffee in there anymore, right? So they set a limit at five. Can't have more than five hazelnut shots in your coffee. This is arbitrary, right? You can imagine them having six and everything's still working just fine, but they set it at five.
[00:08:28] So if they set it at five, I could put a rule in that says, well, I'm already at five, if I add another one, it's just gonna be a no op. I've got five hazelnuts and I try to add another one. It's a no op. Doesn't do anything. But notice this breaks our property cause if I'm at five, I add hazelnut and then I remove hazelnut, I'm now at four hazelnut, so I'm not at the same one as I started with. So it actually makes sense to not put that limit in this operation, to let it go to six. Somewhere else, you can validate that you haven't exceeded all of your limits, but in this model it makes sense because we get to keep this nice property.
[00:09:18] No matter how many hazelnuts I add, if I remove the same number of hazelnuts, I'm back where I started. And that's a really nice property. And then you could go further, complicate it a little more, and say, I can add some set of different add-ins, hazelnut, a soy, two espresso shots, and an almond. I can add them in any order, then remove them all in any order, and I'm back where I start. That is a really cool property to be able to state.
[00:10:04] And you can do this because you're thinking about how things compose before you've started implementing it. And this is one of the things that lets your, that, that ensures that your model is very expressive. You can express all sorts of things and you know that you can reason about their behavior. Because you've guaranteed that they have certain properties when they work together.
[00:10:40] Let's look at the opposite example. If I remove hazelnut and then add hazelnut, that property doesn't hold. I'm not gonna get back to where I started because I might go down to zero. I might go down negative, have negative one hazelnuts, and that turns out is not really, I mean, you could, I guess, do the same thing and allow negative numbers so that later you can validate whether you're negative.
[00:11:14] It might get confusing for the user who's like, how can I even have a coffee with negative? I can understand a coffee with six hazelnuts, but I can understand a coffee with minus one soy. My personal opinion is to not put that in there, to just bottom out at zero and just not have the property. Because it seems like you're violating some invariant of how coffees work by having a negative soy. Having six soy shots does not violate how coffee works. It's simply a practical decision, arbitrary, practical decision that the business made. Okay, we'll talk about that in another lens about scoping and pushing stuff up to another layer.
[00:12:09] And in this lens I want to talk about different well-known properties, things to look for when you're building your own properties. How to test those properties so that you're guaranteeing them while you're implementing. And other considerations, like it's much easier to guarantee properties when you have total functions, which we talked about in the operations lens.
[00:12:46] I like to look at composition and the considerations of these strong properties similar to totality at the function level. Totality means I can pass it any arguments that are valid, I'll get a valid answer. Properties mean I can do any of these operations, and I'll get a valid answer. I'll get something that still makes sense. I'm maintaining the semantics of my model no matter what order of operations I do stuff in. Or which operations I do and how many I do and what combinations.
[00:13:32] My name is Eric Normand. This has been another episode of my podcast. Thank you for listening, and as always, rock on!