What is the "reify to an interpreter" refactoring?

Watch the creation of a simple refactoring to turn functions into data.


[00:00:00] Eric Normand: What is the "reify to an interpreter" refactoring?

[00:00:05] Hello, my name is Eric Normand, and this is my podcast. Welcome.

[00:00:12] Today I want to talk about this technique that I'm trying to formalize. It's something I've seen done a lot. I do it myself, but it's kind of informal at the moment, so I'm gonna talk through it and try to make it a little bit more rigid, I guess.

[00:00:35] What I'm calling it is "reify to interpreter".

[00:00:39] So you have all these mutation functions. We talked about those before. They're functions that take the current value and return a modified version of that value. And these mutation functions are regular functions, but what we want to do is to turn these functions into a data representation of these functions.

[00:01:14] Normally you would do this when the mutation functions represent actions the user is taking and instead of just applying them to the current state, every time the user clicks a button or fills out a form or something, you could just apply it to the current state and you get the next state and the next state, but sometimes you want to record what did they do. And to record it, you really need data cuz a function is a very poor record of what has happened.

[00:01:51] How do you do that? Well, it's a refactoring, and when I say a refactoring, what I mean is it's very mechanical and you can do it without breaking anything and changing any behavior. But then you get the benefits of using data instead of function out the other end.

[00:02:13] I'm trying to figure out exactly what are the steps for this refactoring. You have these functions. You put the current value first, and then any arguments you need after it.

[00:02:26] So the first step is to make a new function that is hard to name, but in the pizza domain, we'll be able to call it something like modified pizza.

[00:02:45] The pizza domain has, in this simple example, four operations. You have add topping, remove topping, set the size of the pizza, and set the sauce of the pizza. Okay? So those four operations, you're gonna make a new function called modified pizza. It's going to take a pizza cuz it's a mutation function.

[00:03:13] So it's gonna take a pizza and it's going to take a another thing that represents the operation that's going to be performed. Now, what is that thing that represents the operation? It's an alternative. So however you represent alternatives in your language, that's what you're going to use. And the alternative is gonna have four choices because we have four operations.

[00:03:45] And so you name the four choices based on the names of those functions. So you translate those names over into however you do it. So in Haskell it would be a discriminated union with four constructors. And you would probably take your lowercase names and uppercase them. So the function names would be lowercase, but now you can uppercase them into structor names.

[00:04:16] Okay. In Clojure, I would take the symbol that names to function, I would just convert it into a keyword and not change the case or anything. And that would be the name of the choice. Right. And then each of those choices is going to have arguments that correspond to the arguments of the operation.

[00:04:46] Remove topping is gonna have the name of the topping to remove. Set size is going to have a size to set it to. Okay, so sometimes you would have three or four different arguments. We don't happen to have one in our example now, but however many you have, you have to put those into the choice for that alternative.

[00:05:12] In the body of that function, you're going to switch on the different choices. Of course this depends on the language you're in. You could have a case statement, you could do pattern matching. If the choice that we've got is add topping, we're gonna call the add topping function with the values from inside the add topping choice. And if it's remove topping, we're gonna call the function remove topping with whatever arguments are inside of that piece of data. And that's it. Now it's a a working interpreter.

[00:05:55] You have a data representation of the four different operations, and you have a mutation function. The first argument is a pizza. The second argument is some operation in a data format that you can run. This mutation function, we called it modify pizza. And again, we talked about mutation functions , how interesting they are because you can say reduce over a list of operations, and they operate on the same pizza serially, one after the other.

[00:06:31] This is a refactoring , I wanna make it as mechanical and automatable as possible because I want to show that this is not some magic trick. Interpreters are not hard to do. They're not complicated. Once you've got the operations, turning it into a data representation, it's very straightforward. And I don't want to overcomplicate it.

[00:07:01] My name is Eric Normand, and this has been another episode of my podcast. Thank you for listening, and as always, rock on!