Modular Code

Its often easy to write monolithic code. As always this revelation is one that is perhaps trivial to many who are deep into the development side of things but may not be as clear to those who use code as a little more than a tool. Here I want to provide a demo of one particular piece of modular code I wrote years ago but that I still use regularly. The point of this is not the specific use of this code; rather, it will hopefully be illustrative of some good practices and techniques. This will also end up being pretty python specific, though some of the principals may be applied to other languages (do be weary of this however, languages should be programmed in ways that are proper for that language and not in ways adopted from other languages).

So, here is the much talked about code block:

def rk4_step(y0, model, *args, **kwargs):
    k1 = kwargs['h']*model(y0, *args)
    k2 = kwargs['h']*model(y0+k1/2, *args)
    k3 = kwargs['h']*model(y0+k2/2, *args)
    k4 = kwargs['h']*model(y0+k3, *args)
    return y0 + (k1/6)+(k2/3)+(k3/3)+(k4/6)

this is a Runga-Kutta fourth-order integrator. Or, more properly its a function that will compute one step in that integration. There are points to be made about why I did not include the loop and the step in one function; however, I have made those points in previous posts (the jist is that functions should do one thing and I content that an integration step is a different thing from asking the computer to do many integration steps so they should be handled separately)

The meat of the modularity here can be broken down into three parts

  • passing a function (model) as an argument to rk4_step
  • using *args
  • using **kwargs

Let’s look at each of these. For the first, passing a function to rk4_step we instantly gain a lot of modularity. We could define any function we want (so long as it has the correct argument list) and pass it at call time, instead of having to worry about that function’s details while we write the integrator. This can be generalized to, try to write code that is as oblivious as practical to the implementation details of the functions it calls (the computers scientists will be shouting about abstraction here but we can stay a bit more….concrete than that).

What about *args and **kwargs. Those are really a furthering of the abstraction from using an function as an argument. They allow us to pass arbitrary positional and keyword arguments to rk4_step, and then continue passing those along to called functions within it. Really we are only using *args in this way here. Note how when we call model we always call it with one positional argument (y0+something where something can be 0) and *args. Python interprets an * in front of a variable name as “Argument Expansion”. Let’s see an example

args = ["a", "b", "c"]

def func1(A,B,C):
    print(f"{A}, {B}, {C}")
func1(*args)
Out[1]: a, b, c

What we can see in this example is *args has expanded the list args out to fill each positional argument that func1 takes. This becomes even more useful when we want to nest functions (as in rk4_step with model) and not worry about their implimentation details in the middle function.

rk4_step takes some set of positional arguments as *args from whatever is calling it, then passes these right on to model. As the middle function in that sandwich, rk4_step never has to know what is in args, how many arguments there are, what their datatypes are, nothing. This allows for model to be even more general as we can change the model function and dont even have to keep the same parameter list (as long as it takes some y first and then some positional arguments). As long as the rk4_step caller knows what to do, rk4_step wont get in the way.

**kwargs is similar but for keyword arguments and dictionaries instead of positional arguments and lists. In rk4_step we just use it to not worry about what arguments get passed to rk4_step and we never pass it along to the model function, but if we wanted to we could and it would have a very similar benefit.

So this was all somewhat…abstract… I should explain more explicitly why this matters. This rk4_step integrator has been used, without modification to integrate all sorts of ordinary first order differential equations, from hooks law to the lane-emden equation. That’s not a huge feat, there are plenty of implementations built into scipy with are even more general. However, the principal, writing code that can be used in a huge variety of situations is very helpful. When next you need to write anything that calls some other function within it, consider if that could be generalized, if it should be, and then if the answer is yes to both consider actually generalizing that. I hope the three concepts discussed here can be of some help in that process.

Leave a comment