What is an action? (better edit)
This is an episode of Thoughts on Functional Programming, a podcast by Eric Normand.
Functional programmers divide up their code into three categories: actions, calculations, and data. Actions are everything that can have an effect on the world, or anything that can be affected. In this episode, we go deep into what it means to be an action.
Eric Normand: What is an action? At the end of this episode, you'll understand this fundamental idea in Functional Programming and you'll be able to recognize it in your own code and begin to deal with actions in a better way.
My name is Eric Normand and I help people thrive with Functional Programming. In my definition, my understanding of Functional Programming, the first step of doing Functional Programming is to divide things into three categories. Divide everything in your code, everything in your problem domain into three categories.
Those categories are actions, calculations and data. This episode is about actions. These are my terms for these categories, but it's something that all functional programmers do, is they see actions, calculations and data, they distinguish these things.
You might have heard actions referred to as impure functions, or sometimes people will talk about the effects or side effects of a function. These could also be loosely looked at as actions. Actions are anything that depend on when they're called, or how many times they're a called.
Let's go over some examples, because that's like a rule of thumb for identifying them but it might not help you understand what actions are. When we run our software, the reason we run software is because it's going to gather information from the world and make some kind of decision and then act on the world again.
An example of this is, a computer system or Web server is waiting for a user to create an account. They're submitting some Web form, and then they create the account. They save that information somewhere to a database, and then it sends, an email confirmation. The important stuff that the software does, the reason it's running, is all of those external effects it's having on the world.
The sending of the email, the saving of the informations that they can log in afterwards, the accepting of the Web requests, what happens between those things is, you could look at them even it's like it's unimportant implementation details, how it actually does it. What's important is the effect it's having on the world. That's why we run our software.
These are all actions. The sending of an email is an action. If we look at it from our rule of thumb, the rule of thumb says it depends on when it is called, or how many times it is called. You could say, the email definitely depends on how many times it's called because if you send the same email twice, that's different.
If you send it a hundred times, that's really bad. If you don't send it at all, you send it zero times, that's also bad. You want to send it exactly one time. Other things depend on when they're called.
Let's go over some more examples of actions. One form of an action is reading from a mutable variable. That's an action because it depends on when it's called. Every time you read, you could get a different answer because something could have changed the value in that variable in the meantime. That's what mutable means.
Reading from that variable is an action. It depends on when it's called. When you read it, you're being affected by the whole history of every right to that variable. You don't know what your thing is going to read until you read it. Contrast this to, let's say, calling a pure function or calling what I call, a calculation.
It's a function that does not read from mutable variables. It also will give you the same answer every time if you give it the same arguments. This is like a mathematical function. If I say, A + B, I'll always get the same answer if A and B are the same.
If I say, 7 + 2, I'll always get 9, no matter how many times I call it, or when I call it. It doesn't matter. I'll get the same answer. Let's look at another action, which is writing to a mutable variable. Does this depend on when it's called? Yes.
If I call that, if I write to this mutable variable just before you read from it, then it does matter. If I'd call it after, you'd get a different answer. It does matter when I write to it. It's an action. The real trouble that these actions cause...They cause trouble. Let me put it that way. They cause trouble.
They're simultaneously the reason we run software, and the most troublesome parts of our software. Why do they cause trouble? When you get down to it, actions, because they are so dependent on time, how many times they're run, or when they're run, what order they're running, it really makes it hard to know what's going to run next. What's going to happen next?
It could be this thing, or it could be that thing. You don't know because it all depends on what's happened in the past. Sometimes, it's even quite chaotic and random what's going to happen next. If you have two threads, and they start interleaving with each other, it's very hard to know what's going to happen next. That makes it really hard to program.
We're trying to make these very reliable systems, and things are just happening in different orders every time we run them. There's a lot of chaos in that system. It's very complicated. This is where a lot of bugs come in.
Part of this understanding of functional programming is that we recognize actions, and we do our best to use them only when necessary. Basically, as little as possible. What does that mean? We have to send an email. That's part of the business requirements. That's why we're running the software.
We want to send that email. It's an action but we want it to happen. That's a necessary action. We can't get rid of that. We can't eliminate that action. That reading and writing from and to a global variable, that is a choice that we're making as implementers. The user doesn't care if we use a global variable.
That's what we chose for ourselves. We actually could eliminate that and not change the meaning of the software. If we do that, we're eliminating an action that wasn't necessary. That's one of the first things that we learned to do. It's to move from using mutable things, mutable state, to a more calculation-based approach.
We're using many more pure functions, mathematical functions. There's a number of techniques for converting these inessential actions, the actions that we chose to use as an implementation but weren't really required, when to convert those into other things like calculations, and sometimes even its data.
Then there's some actions left. You still have to send that email. There's also techniques for dealing with that, for managing those actions that are left. Because they could still cause trouble, and we want to ensure that they don't. We want to ensure the system still works the way we want it to.
These are the hardest to explain. Functional programmers have a lot of techniques for doing this. Let's say that we do want to have a little bit of mutable state. We're functional programmers, we know the risks, but want to mitigate those risks. We have this bit of mutable state. How do we make it easier to work with?
One trouble with mutable state is if you have one thread, or one part of your system writing to that mutable state, and when you're halfway done writing, you're not done yet, but you're halfway done, some other part of your system reads the value.
They're reading half-finished work. They don't know it's half-finished, they just know what they read. That's a really big problem. I'd rather you have read an old value than read this half-value that isn't even a real thing. It's not what I intended to write, but you were so fast and I was so slow. Boom, you just read it out from under me before I had the chance to finish.
What we have in functional programming is techniques, models, patterns for making this into a transactional write. Instead of being able to see my work half-finished, we can ensure that you either see the old one, which was finished, or the new one, which was also finished. There's never an in-between state. We make it transactional.
If I am writing to it, I'm halfway done, then I mess up and I have to throw an exception, it's going to go back to the old one. Other parts of the system are still reading the old one. I never get to this half-completed state.
What that does is it eliminates all sorts of trouble. You still have a choice. You still have some trouble. Things might be reading the old value, it's still mutable state, they're going to get a different answer every time they read, or they could get a different answer every time they read.
You still have some of this complexity, this chaos of, "What's gonna run next?" Let's say, I put a Boolean in there, and you make a decision based on that Boolean. Now, you're branching. I don't know if the "then" branch is going to happen, or the "else" branch is going to happen.
One of them is, but I don't know which. It's still branching. There's still like, "I don't know what's gonna happen next." Way fewer things can happen next. Instead of some weird intermediate value that might be broken, it might cause some other branch to happen, it's just the ones that I intend can happen.
It's less complex, even though it's still complex. Imagine another type of action, an email. You have to send an email. This email server is on another machine, so we have to send a message to this machine to send the email.
Most of the time, when we send the message, we get a response saying, "Yes, I sent it, good job, it all worked." Every now and then, we don't hear back. Send a message, and the request times out. We waited a long time, we don't hear anything back.
What happened? Did it send the message, and then crash? Or, did our request never get there, so it never sent the message? We don't know. Did the message get sent or not? We have no idea. It could be either. We want to send this thing again, just in case, right?
Then, if we send it again, it's going to be either it sends it one time, or it sends it twice. We don't want that. We want it to be exactly one time. We need to manage this. We need to make it so that we can control how many times it sends, and make sure it's exactly once.
Functional programming has a technique for this. What you essentially need to do is make the action idempotent. Make the message idempotent. When I send this message, the server will recognize whether it has already sent this particular email already. If it has, then it just says, "OK, good." If it hasn't, then it sends it.
That will allow my system to send the same message twice without any problem. I can send the message to request that email be sent twice in case there's a time out, in case there's a networking issue, in case I crash in the middle and I can't remember where I was, and I'm just trying to recover.
This is another technique for controlling the number of times that this email gets sent, and divorcing it from the number of times I request that it be sent. This is another technique that functional programmers use to manage time, to manage these necessary actions, and make sure they do what we expect with the least complexity possible.
I'm going to recap. There are three categories that I think are the primary distinction that functional programmers make. It's actions, calculations, and data. Actions and calculations can both run their code. Data is not code, it's just data, it's inert. Actions are code that depend on when they are run, or how many times they are run.
We run our software for the actions, because those things can actually have an effect on the world. There are some actions that are not essential. Actions are so problematic, we'd like to get rid of those, and make our software more reliable.
We eliminate as many actions as possible, replacing them with calculations, and then we manage them. We have some techniques for making actions less complicated, less complex, less tied up in time.
Cool. My name is Eric Normand. This has been my thought on functional programming.
I've got a book coming out. It's all about this. This is chapter three stuff. Chapters three through six are all going to be about managing actions, making sure they do what we need them to do without introducing more complexity than they need to.
I just want to let you know that that's coming out soon. It might already be out by the time you watch this, so do search for it. The book is called "Taming Complex Software." Give it a good search.
Cool. Thanks for being there, and rock on.