Positional Arguments Suck

Posted by Jake Corn on May 6, 2020

Positional Arguments combine the ideas of Position and Meaning


function updateUser(firstName, lastName) {

One has to remember the order of the parameters to get it right.

updateUser('Jones', 'Sal')
// oops...!

What if we used an anonymous object? It's easier to get the arguments right because there are names to guide the caller

  firstName: 'Sal',
  lastName: 'Jones'

Positional Arguments are a slippery slope

One universal truth in software is that requirements change. It's a good thing -- it's why we have jobs at all.

Imagine requirements change and updateUser is now expected to take in other user information

you update to take in age, gender

updateUser(firstName, lastName, age, gender) {}

and you move on... calling the function is a little harder than before, but who can't remember four things.

3 months later...

updateUser(firstName, lastName, age, gender,
eyeColor, hasDriversLicense, ownsAHome,
workAddressLine1, workAddressLine2, workAddresCity,
workAddressState, homeAddress...) {}

I've seen it.

Here's why it happens...

the function gets written with a small number of parameters and a small number of callers. changing it is easy. Someone else updates it for their use cases and moves on, so on and so on.

Adding on another argument is easier at this point than aggregating arguments in a data type.

When should I use a data type over positional arguments?

As soon as a function requires more than 1 primitive value (string, int, etc...).

Before this point, using a data type could lead to confusing naming.

After this point, the amount of work climbs per parameter.

Positional Arguments prevent new logical data types from arising in the system.

Back to the updateUser function.

ALL of its formal parameters describe some aspect of an entity in the system... User.

this example is pretty obvious due to naming, but let's try an arbitrary example with a poorly named function

foo(purchaseDate, size, distribution, brand) {}

we have no idea what 'foo' means. the args suggest it could be used for anything.

which may be a valid case. Not all the time are our domain object very well spelled out.

If we can't find a good name an "arg" data type is still permissable.

interface FooArg {
  purchaseDate: Date
  size: number
  distribution: Distribution
  brand: Brand

}: FooArg) {

This at least allows us to observe FooArg as a unit.

Maybe eventually we find that everyone who is using a FooArg type does similar things with it (highly likely)

and we find we can easily dry up our code base by creating a FooArg class (or similar).

class FooArg {
  buildReport(): FooReport {}
  isActive() boolean {}
  //.. etc

Sure, it's just code, but this also gives us more language to describe the state of the system -- which is arguably more valuable...

But, But, But... Variadic functions...

Okay, that might seem different but it is actually a function with an iterable collection argument in which case it begs the question -- What is the syntax giving us that a list couldn't achieve?

The answer is, not much

take lodash omit for example...

omit(obj, 'prop1', 'prop2', 'prop3')

// vs

omit(obj, ['prop1', 'prop2', 'prop3'])

Let's have a faceoff...

Lists (arrays, collections) vs Variadic functions

If you only could have one of those abstractions which would you choose?

Another question may be, "Why are you asking us to choose?" Because if you don't you'll be casting one to the other frequently. Which will cause...

Broken Windows

"Broken Windows" is a term I took from the book "The Pragmatic Programmer".

Broken Window Theory

It may or may not be valid but the idea is that if one thing is allowed to stray from what is viewed as positive behavior, more things WILL stray.

in our case, if you allow one function to be variadic then you are setting the stage for more forms of positional arguments, most of which will not be as innocent as the variadic function.

-- jake