BrainsToBytes

Better arguments == Better functions

Arguments play a very important role in the readability of a function. When implemented properly, they show intent and provide information about the function's behavior. This is one of the reasons why investing time in engineering good arguments can help to improve the quality of your code. Let's see how we can make the most of our arguments, or at least ensure they won't get in the way.

Keep the arguments < 2

The fewer arguments a function takes, the easier it is to understand. If you've been programming for a while, I'm sure you've dealt with argument behemoths: Those horrible functions that take a huge number of seemingly unrelated arguments. Understanding how all those arguments work together to get anything done is a very difficult task.

What's worse is that in most situations, those functions shouldn't even exist. Usually, they are a sequence of steps you can easily pack into smaller functions with one or two arguments. Long lists of arguments are usually a sign that a function is failing to do only one thing, and do it well. If you find a function like that, try to re-engineer it into a series of smaller, well-defined functions.

Ditch all arguments: the joys of niladic functions

What's the best number of arguments function can take?

Easy, 0.

Arguments are hard on the brain: they force you to consider additional information when trying to understand what a function does. The different values arguments can take, the level of abstraction they belong to and many other things encumber your efforts to understand a function. Having no arguments saves you a lot of cognitive work. Niladic functions usually come in two flavors:

  • A function that triggers an event, which changes the state of the system or sends a message to another part of the application.
  • A function that alters the state of the caller, or returns some information about it, like toString or isEngineOff.

Nilads are not only easy to understand (well-written ones, at least), they are also significantly easier to test: verifying the function works correctly with different combinations of arguments is trivial if you don't receive any.

If you can solve your problem with a niladic function, go for it.

Writing good monadic functions

After 0, the best number of arguments is 1. Monadic functions (functions with 1 argument) are significantly harder to understand than niladic functions of similar complexity. Still, functions that take 1 argument are totally fine, and your project will probably contain a huge amount of these.

Monadics are usually one of the following:

  • A function that returns information about the argument, like is_green(screen_background).
  • A function that transforms the argument into a different thing and returns it.
  • A function that triggers an event (same as above).

There are two important things to consider when working with monadic functionss.

The first one is to avoid output arguments. A function has an output argument when it doesn't return any value and mutates the state of the input. For example, the function:

public void uppercasePersonName(Person person){
    person.fullName = person.fullName.toUpperCase();
}

In this case, person is an output argument, whose state (fullName) gets changed. Functions with output arguments can be hard to understand and are a good place for bugs to hide. If possible, prefer pure functions.

The second thing to consider is that, if possible, you should transform monadics into niladic functions. Think for example in the function add_reference, which appends a reference to an article object:

    add_reference(article)

We can refactor our code so that article objects respond add_reference:

    article.add_reference()

In many situations, this also results in classes with better public interfaces.

Two is company, three's a crowd

More than 2 arguments make a function significantly harder to understand. You want your function's singature to read like clear prose, but understanding how a function name and 3 or 4 variables work together can be daunting. When the arguments don't have an obvious connection with each other, understanding the function becomes even harder. Consider the following function:

    # Understanding this function is not that easy, it takes at least a couple of seconds to get an idea of what it does
    def draw_cilinder(x, y, radius, height, renderer, texture)
    end

Of course, not every function with more than 2 arguments is problematic. Imagine you are working on a 3D graphics application, sooner or later you will need a function that gets 3 arguments for the position of a point in space. The arguments have a natural ordering and it's easy to understand why they belong together.

There are some ways of improving the readability of a function that takes many arguments:

Ditch flags

Receiving a flag to determine what happens in a function is something you should avoid. Instead, create two (or the necessary amount of) functions and call them accordingly.

A function that checks a flag to decide what it should is doing more than one thing. You want your functions to do only one thing, and to do it well, so ditch those flags.

Compact arguments into a single object

Often, functions that take many arguments might be an indicator that your program needs a new class. In the example of a 3d application, instead of receiving 3 arguments for the positions of a point in space, you might want to create a Point class and pass that as an argument instead.

Bundling arguments together should not be abused, you could end up with frankenobjects made of loosely-related pieces. Think about proper abstractions and create the necessary classes.

Write better names

You can't always reduce the number of arguments. When all else fails, giving them proper names can at least make them much easier to understand. Think about the following snippet:

    insert(article, epilogue)

    insert_epilogue_into_article(epilogue, article)

Which one is easier to understand? Well, both functions obviously perform an insertion action using those two arguments, but I find the second one less ambiguous. It's ok if it is a bit wordy, typing is cheap, so lean on the side of verbosity.

Giving functions a name that ties an action to the arguments can make it way easier to read, so go and give it a try.

Remove argument order dependencies using keyword arguments

On the early days of a project, things like the number of arguments and their order are very likely to change. In this stage, you want to make it as easy as possible to experiment and make changes. Removing the friction to make changes encourages you to improve the design and move things around, one way of achieving this is removing the dependency over the order of arguments.

Look at the following constructor:

    def initialize(radius_in_mm, number_of_frets, fretboard_material, scale_length_in_imm)
        @radius = radius_in_mm
        @frets = number_of_frets
        @material = fretboard_material
        @scale_length = scale_length_in_imm
    end
    # ...
    Neck.new(254, 22, 'ebony', 647)

If we make changes on the order (or number) of arguments, we will need to update every place where we call this constructor. While not hard, there is implicit friction in performing this change. We can instead use keyword arguments like this:

    def initialize(radius_in_mm:, number_of_frets:, fretboard_material:, scale_length_in_imm:)
        @radius = radius_in_mm
        @frets = number_of_frets
        @material = fretboard_material
        @scale_length = scale_length_in_imm
    end
    # ...
    Neck.new(radius_in_mm: 254, 
             number_of_frets: 22, 
             fretboard_material: 'ebony', 
             scale_length_in_imm: 647)

As you may have noticed, this is a tradeoff: we removed the dependency on the order of arguments, but now we depend on their names. In most situations, this is a good tradeoff, as the names of the arguments are way more stable than their order.

For most mature applications, keyword arguments are overkill so you may decide not to use them. For projects on early stages of development, it's a powerful way of keeping the code flexible and responsive.

Isn't it too much, they are just arguments?

Yes, I know it seems like too big a thing just for humble arguments, but it can help to make your code more readable. When reading a function, the first things you see are the function name, the arguments (and if you are lucky, the return type). Giving the function a good name is obviously important, it's a lesson all software developers need to learn: good names are extremely valuable.

Arguments are the missing piece for making the signature of a function fully expressive. Spend a little time and experiment until you get the results you need. A couple of months in the future, when you forget the details of your implementation, you'll be grateful for the extra minutes you spent in making your function signatures easier to understand.

What to do next:

  • Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
  • You can find more information about creating good function arguments in chapter 3 of Clean Code, and in chapter 7 of Code Complete. This and other very helpful books can be found in the recommended reading list.
  • Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!
Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!