How to avoid premature optimization?

This is an episode of Thoughts on Functional Programming, a podcast by Eric Normand.

Subscribe: RSSApple PodcastsGoogle PlayOvercast

I explore why clean code is a lagging indicator and how the domain model is a leading indicator of maintenance cost.

Transcript

How do we avoid premature optimization? Hello, my name is Eric Normand and this is my podcast. Welcome. I'm still talking about domain modeling.

One of the things that I do when I'm not at my best is I tend to approach a problem by jumping right into the code, and I believe other people do this too. I think it's very common. When you jump right into the code, you necessary or very likely are baking in a lot of assumptions that are really not safe to make it. A lot of those are premature optimizations.

When we talk about premature optimizations, we're often referring to stuff like you unrolled your loops too fast or you thought that the bottleneck would be here, but it's really over there so you really didn't need to optimize this piece of code. Those are all premature optimizations, but there's other optimizations that perhaps go unnoticed or go unanalyzed.

One thing that might be an optimization is if you're developing an image library. You might begin by thinking, "Well, how do I best represent that image?" It will be a two-dimensional array of colors like bites that represent colors. That also is a premature optimization.

You have chosen a data structure very, very early. Presumably, because that is faster or maybe it's just because everyone else does it that way and that's how you figure they must've thought it through, and that was a good way to do it or you just didn't even think enough about it. You just did what other people did.

There's a bias there that we lean on what others have done before. That's the premature optimization that I think that a good process of domain modeling can avoid.

Just in general people in general, but programmers specifically we optimize too soon. We think of a concrete representation too soon. Once you've got that concrete optimization, lots of stuff becomes our concrete representation. It becomes very hard. You don't know that yet until you start coding in it. PRISM makes a lot of stuff very easy, like looking up the color of a particular pixel is easy.

But what about looking up the color between two pixels? If you zoom in, you want to blow up your image, what pixel values do you put in there? Oh no, now it's not so easy. Then, you have to make a copy that's twice on both dimensions. It's four times as big. You're making a copy of the bigger A. That's not so optimized anymore.

We need to slow down and have a step before we choose at least one step, a phase, a whole phase before we choose the concrete representation. That's what I want to present in my potential upcoming, maybe I'm going to write a book.

What does that phase look like? It's basically using algebraic thinking to explore what is really required in the domain. After you've done that, then you can start implementing. What you develop out of the first phase, it could be a spec -- a specification.

Like I said in the last episode, you can encode that spec in a programming language or in English or some other language, even just pictures. You can encode it however you want.

I believe it's a good idea to encode it in a programming language because it's in a programming language. You get a nice benefit, which is that you can run it. You have a runnable specification. Then the implementation is all about optimizing it, making it usable in all the ways that it needs to be usable.

What do I mean by that? Maybe you need to store the thing in the database and you just did it all in memory, in your model. OK fine. Like "Let's implement it so that instead of running over a data structure in memory, it's making a SQL query" or whatever it is.

That's all secondary. Primary, it's to get it working in memory. This has a lot to do with live coding. We have these really powerful tools. We have a programming language, fast compiler, really fast computer with a ton of memory. We can run our specification, as a naive program, and it will probably work. It'll probably be good enough to run.

You can see, you can play with it. You can see if it really does what you need it to do before you start deciding on concrete implementations before you start making it work and all in your stack, but the database, and it's got to be async because it needs to run in the browser and all that stuff.

Again, last time I talked about how I'm trying to identify these leading indicators that are even before clean code, what's typically called software design, before that, way before that.

Identifying your operations and making sure that you've got a complete model of everything, trying to get rid of all the corner cases, all these leading indicators like number of corner cases, whether you have a good fit between your domain and your model, whether you've got all the operations possible.

All these things are leading indicators that your code is going to eventually down the line be easier to maintain. The thing is, though, if you focus just on the maintenance, they're so far down the line, you're not going to be able to hit them directly.

We need to just put that aside, maintenance put it aside, efficiency, put it aside. Even how could this possibly work, put that aside? In this first phase, you need to clear your mind of all that stuff, all those constraints, and really get down to the essence of your domain.

What is it that we need to do? Those are your operations. What information do those operations need? What are the invariants that the operations and the data need to follow? Invariants are things that maybe relate to pieces of data or they relate how two operations work together or need to work together.

They might be simple like this has to be a positive note. That's a simple constraint, but it could also be that this one needs to be five times that one, or if we have this one, then we also need this one. Or it could be something like this function is the inverse of that function.

These two operations are inverses. That's also an invariant that needs to be maintained. We need to step back from...I don't want to say from the code but from implementation.

There's a fine line that you have to walk where you're writing code because you're writing this runnable specification, but you have to be carefully not going down the implementation road, staying in a much more abstract place. Those abstract things are things like the signatures.

If you think what are the pieces of data, I need to do this operation? What will this result in? You get a function signature. If you have a typed language, you can write that out as a type, you can write it out as a pseudocode type if you don't use a typed language.

It's abstract because it doesn't have an implementation, it's just like I know that I'm going to combine these pieces of data into...I call it data. That's even maybe two. That's a good thing to note. It's not about data. You could be combining other operations with a higher-order function.

You're combining two things in your model [laughs] or multiple things in your model into another thing. If you got types, you can think about them as types. I like to think about them as types, even if I'm not using a typed language. I believe that types are very useful whether or not your language gives them to you.

We often think in types, even if we don't have a type checker in our language. You want to not think about like, "Oh, this has to be data." It might not yet. You're working on this abstract level, where you're talking about types and the relationships and the invariants between them, and how this operation converts or composes these things into a new thing.

That's algebraic thinking. Working through all that, you can see corner cases, right away. It's magic. You can avoid them by saying, "Maybe this type doesn't really capture what I'm trying to say, "Maybe I need something else." You work through them, figure out, "OK, I can do this, there's no corner cases. This is a total function. I've got all of the ones I need."

Look, also you start to see something like, "I can implement these five in terms of these three, so I really only need these three right now. I can deal with these later."

You're starting to minimize the surface area of your domain model. That's a good sign that you're finding some essence. Domain models like the prototype that we're looking for, is something like Newtonian mechanics that has a minimum of concepts. Really, it only has...I don't know five concepts and then it has three laws that relate them. These are the invariants.

You have forces, which come in pairs. You have position, time, then velocity is actually defined in terms of position and time. It's DX over DT. I guess you need derivative. That's an operation that you need. You can see how then acceleration is actually DV/DT. Force is the sum of the forces is equal to the mass and the mass times acceleration.

You really only need mass, position, and time. The rest are defined through invariance. That's the thing you want to find is some small set of operations and pieces of data, and invariants that you need that totally define the system.

Newtonian mechanics is a complete system, doesn't describe everything in the world. That breaks down at the speed of light, or near the speed of light, it doesn't work. It doesn't talk about size. It's all point masses, but it is self-consistent.

That's what you want. If you need to deal with size, that's a different domain. There's this idea of scoping. What belongs in your domain, and you have to explore like, what if I bring in another concept and that doesn't really have invariants?

It doesn't have relationships between the other pieces of data. Remove it or what if I take this thing out? Oh, well, no, then the invariants just don't make sense anymore because they're all related through that. I have to keep it in.

Your scope, you're constantly exploring that scope, you're exploring the surface area of it. This is the thing that I'm going to have a lot of trouble talking about in the book. There's a kind of...I want to call it magic. [laughs]

There's a something will click, there's insight in it, like it's insightful to leave that piece of the domain out, because wow, look at what gets much simpler, much more elegant. We can always add it in with another layer on top right, with another model built on top of this one, or side by side with this one.

There's a click and you have to sensitize yourself to that intuition, that insight of complexity and simplicity of how it's expressed and the size of it, and the scope of it. There is a real magic to that. I forgot to mention this. A lot of these ideas are inspired by Conal Elliott. He has a thing that he calls denotational design. Denotational design, like I said, I'm inspired by it.

This is not denotational design. The reason it's not is because he's way better at math than I will ever try to be. I wish I could be like him. It seems really cool to be able to do what he does. He takes the algebraic thinking to the next level.

I'm hoping to take just a tiny bit from that, not go as far into the mathematical side, but tap more into intuition, about the scope and the size and the API service.

It was his idea to say it is premature to call an image grid of pixels. The way I think of it is clear your mind, Zen out and think what is an image? What is it? That is something we can do because we are all kind of domain experts on images we see, and we've probably at least as kids colored and drew pictures, and we've seen a lot of images, we've seen TV, we've seen paintings, etc.

We're kind of all domain experts in that what is an image? His idea is it's a mapping from a location in 2D space to a color, mapping from the location in 2D space to color. Now, notice he scoped it. He's thrown out the idea of say, a frame. Like it is say, rectangular or circular. You can have circular images. It doesn't have borders.

It just maps location to color and he doesn't talk about it in the talk, but I feel he has explored and chosen to scope it down lower and say, "We'll deal with putting it in a frame later. We'll deal with pixelating it later," which is basically sampling this function from location to color.

We'll deal with that later. When we have to put it on a screen, we can do that. He's Introspective into the idea what is an image and used his knowledge, his domain knowledge of that, and scoped it. Really gotten it to the right size where he can say, "Well, now that it's just a function. I know a lot of things I can do with. I can apply a bunch of algebra to it.

I can start defining operations. "Oh yeah, look. I can define an overlay operation that puts one image in front of another, and I know how to represent that as another image." I found another function from location to color, which basically is just saying, "Well what happens if I take this color and this color and I overlay them."

There's some way of mixing the color depending on alpha channels and stuff like that. He's deferring that to like the color domain.

He's scoping this and playing with the scope, and he's found something where now defining the operations starts to feel nice. This is the intuition and the magic that I was talking about, witchcrafts that we're doing.

We're taking these ideas and boiling them down to their essence, and now what can we do with them? He's writing it all out in code as it happens to be Haskell code, so he can write out types and things. It feels nice once he's got it to the right spot.

I talked about the magic and the insight is, "Well, let's just punt on an extent to this image. We can make the image infinite. Maybe there's nothing there. Maybe there's it's clear that Alpha channel is all transparent around here, on the outside." We don't want to prematurely say, "Oh, it has to be within a rectangle," or some extent, even any extent. We'll let something else deal with it.

This idea of stepping back, in this case, it's like freeing your mind to just "No, don't think about implementation, don't think about optimization, don't think about practicality, maintainability, all that stuff. Just think about the essence of it." That's the first phase. Then write it down. Write it down in a language that is precise enough that it keeps you honest.

Then make sure that it's complete, that you've got all the operations you need. That you're sure that you can define this one in terms of that one, and you got to make sure you can do everything you need to do. He was sure, "Oh, I can put it in a frame." He was sure of it.

You can imagine how to do it. You can imagine how to pixelate it. I know how to do it. Then simplify, play with the scope, play with the representation, the type. Play with what is inside, how the operations relate to each other. Does it do what you want?

Now, the cool thing is, this is what I don't think he does this, he doesn't show it in his talks, but what I think is cool is that because you can write it in code, you can run it even though it is way before even implemented.

You haven't chosen a representation yet, but it runs because you have written it precisely in a programming language. That's amazing. You can actually play with it and see if it not just like into it. Is this enough or will this work?

You can actually see it. [laughs] You can do it and find maybe a faulty assumption just by writing an expression in this thing and running it and seeing, "No, that's not really what I meant."

I talked about two things, this need to back up, think more abstractly about your domain, get precise, complete, and simple. Those are Conal Elliott's ideas. I wasn't sure whether I'd use them, but they seem to be working.

This is way before implementation, way before choosing an optimization. Using a lot of algebraic thinking, and then there is this other piece that you can use, you can build in insight. Last time I talked about how that there's something about a domain model that can give your business a business advantage, like a competitive advantage. Not just like a cost advantage.

Our software is cheaper to maintain than your software, but that we have an insight into the business domain, that lets us do things better. This could be we have an insight into...If you're Starbucks, making coffee or selling coffee or whatever your domain is.

We have an insight into it that lets us do it better. We have an insight into accounting, and your needs as a customer, that customer of accounting that we can do it better. It's that insight of scoping, figuring out what you're dealing with, and having this elegant small API surface that cleanly without corner cases or anything can do the thing.

That's the two things, there's this working abstractly in a programming language and then, insight, building an insight early. I do have time. I'm going to tell a little story that I heard on a podcast. I have a friend who has a podcast. It happens to be a Ruby podcast. Now, I got to look up. The podcast is called Code with Jason, by Jason Swett.

On a recent episode, he was talking about a business domain that they were dealing with, and it was all about documents and faxing and stuff. Though they had in their model, they had a class called Fax.

He felt like something was not right with it. It didn't seem to want to attract methods to it. It didn't seem to fit anywhere, and he realized, "Wait, there is no such thing as a fax. There's a document, and you can fax the document, but there's no such thing as a fax. It's not a thing."

It's something that we would say informally when we're talking, "Hey, did you see that fax that came in," or "Did you send that fax to that guy?" We would say that, but the insight that he brought to the domain model was there is no such thing as a fax. We should think about documents and maybe whether they're faxable.

You can't fax a magazine because it's got a spine, but you can fax a stack of papers, or a PDF or something. Whether that's faxable, you fax it as an action, an action you take, and then a document comes out on the other side of the fax machine on the other side of the world, but it's a document again.

It's that insight that we need to capture. We often use these informal ideas, and when we try to make them precise in a programming language like, "I don't get it. Why this doesn't work? This is so weird. Why doesn't it work?" "OK, that's a good signal that maybe it doesn't mean anything. Scope it down." That's the insight that I'm talking about.

Thank you, Jason, for that example. Go listen to his podcast. I have exhausted this topic for now, and I'm out of time. Thank you so much for listening, and as always, rock on.