To be or not and how to be… the existential crisis of parameters

Developers all know how much goes into building software, from architecture to declaring variables, to get to what we (and the business) consider success all too often overly simplified by the mere “making the code work”.

On that subject, a couple of years ago, Tony Deigh (CTO of Jobcase) shared a thought that stuck in my mind: “Code is read more than it is executed”. I keep it in mind as a reminder that first and foremost, code is read by other humans. Correctness (it does what it is supposed to do) and optimization for execution are necessary but not sufficient (permission-to-play if you will).

While updating or simply trying to use a piece of code, have you ever read it and wondered why a variable/member is declared a certain way? Or tried to understand the previous developer’s intent for variables/members?

Here are 2 concrete examples illustrating these key questions in code. Although the concepts discussed here apply to all languages, I have chosen to use typescript and Python to illustrate the points with concise, simple, and concrete examples.

Python:

def create_new_user(first_name, last_name, avatar=None,):
    ...

Is avatar meaningfully different from first_name?

if the function was declared like so:

def create_new_user(first_name=None, last_name=None, avatar=None,):
    ...

Would it mean something different to you?

TypeScript:

interface User{
    firstName: string
    lastName: string
    avatar?: string
}

Is avatar meaningfully different from first_name?

if the interface was declared like so:

interface User{
    firstName: string
    lastName: string
    avatar: string|null
}

Would it mean something different to you?

Functionally, they relatively the same. Why use one form over the other and does it matter?

TL;DR Detailed answers to these questions are at the end of the article.

There is a sizable difference in semantic meaning and, if you bear with me, by the end of this article: you will be able to tell the difference between these declarations and when to use which with discernment to help the next person reading your code.

First, I will go over a few definitions to align (level) our knowledge and build a common understanding of the core concepts. I will then expose the concepts using a few chosen examples to illustrate them as I go. I will continue by contrasting this approach with other common ones. Finally, I will answer the questions highlighted above (in the introduction) and highlight key takeaways.

"Forget Everything You Think You Know" - Ancient One

via GIPHY

A new understanding from the ground up in 2s

Kinds of parameters

The kinds of parameters discussed in this post are:

I am glossing over the other kinds of parameters as at a high-level, they are declension of this subset applied to different use cases.

In Python:

def my_function(
    pos_param_1,
    pos_param_2,
    *,
    kw_param_1,
    kw_param_2,
    optional_param_1="1",
    default_param_1=1,
):
    ...

In the same way in TS as an object, named parameter (React style function signature)

interface Props{
    kw_param_1: string
    kw_param_2: string
    optional_param_1?: string
}

In the same way in TS as a function

function myFunction(
    pos_param_1: string,
    pos_param_2: string,
    default_param_1=1: number,
    optional_param_1?: string,
) {
    // nothing to see here
}

Wait a second… parameter or argument?

You may have seen these 2 concepts used interchangeably in the past. They technically express 2 different “states” in the life-cycle of the same “thing”.

The parameter is the name of the variable at function or method declaration/definition time.

def my_function(parameter_1, parameter_2):
    ...

The argument is the value (or reference) that you pass the function/method at invocation time.

my_function(
    "argument 1",
    "argument 2",
)

What do I mean by semantic meaning?

There are 2 types of semantics in computer science. One is about programming language theory, that is not the one I am talking about there. The other is about the semantic meaning which is the meaning (from Greek sēmantikos ‘significant’) “something” has based on its relationship with others. The dictionary reference for the definition is here.

The salient point here is: what does the declaration of a variable mean beyond the syntactic and computational correctness?

Ready?

"You have to let it all go. fear, doubt, and disbelief. Free your mind." - Morpheus

via GIPHY

Semantic meaning of parameters

Breaking it down

Take this Python function declaration for example:

def my_function(
    pos_param_1,
    *,
    kw_param_1,
    optional_param_1="1",
    default_param_1=1,
):
    ...

Let’s break down what the developer of this function is telling us:

:warning: Note :warning:

Now, let’s take this TS class (named parameters) example:

interface Props{
    kw_param_1: string;
    kw_param_2: string | null;
    optional_param_1?: string;
    optional_param_2: string | undefined;
}

Let’s break down what the developer of this class is telling us:

:warning: Note :warning:

Answers to the introduction

Now, let’s apply our newfound understanding to the examples from the introduction:

Python:

def create_new_user(first_name, last_name, avatar=None,):
    ...

Is avatar meaningfully different from first_name?

Yes, it is different. From reading the declaration of the function, we understand that a user may or may not have an avatar but it needs a first and last name. Executing:

    create_new_user('John', 'Doe',)

and

    create_new_user('Jane', 'Doe', avatar='an avatar')

Will successfully produce 2 full-blown users. We can use the function like that with confidence.

if the function was declared like so:

def create_new_user(first_name=None, last_name=None, avatar=None,):
    ...

Would it mean something different to you?

Yes, it does. It seems like I would be able to use it like so:

    create_new_user()

and I would get a full-blown user… but what user? TBH, I would be suspicious and I would look at the doc and/or implementation to know if the declaration is accurate as I am not supplying anything “special” or “unique” for the user, I doubt we are building a clone army.

TypeScript:

interface UserOmitAvatar{
    firstName: string
    lastName: string
    avatar?: string
}

Is avatar meaningfully different from first_name?

Yes, it is different. From reading the declaration of the function, we understand that a user may or may not have an avatar but it needs a first and last name.

if the interface was declared like so:

interface UserNeedAvatar{
    firstName: string
    lastName: string
    avatar: string|null
}

Would it mean something different to you?

Yes, it is different. From reading the declaration of the function, we understand that a user may or may not have an avatar and I have to explicitly say that the user does not have one but it needs a first and last name.

const user_1: UserNeedAvatar = {"firstName": "John", "lastName": "Doe", "avatar": null}
const user_2: UserNeedAvatar = {"firstName": "Jane", "lastName": "Doe", "avatar": "an avatar"}
const user_3: UserOmitAvatar = {"firstName": "Dom", "lastName": "Doe",}

In these 3 example invocations, the values of avatar are very different:

Common Alternate Approaches

Before closing on the subject, I would like to bring up the key counter-arguments that I balance constantly in my mind assessing which strategy works best.

The argument goes as follows:

I use keyword arguments systematically to make my code more maintainable

I have seen 2 variations of how it comes to life.

Some exclusively use it at invocation time and declare the parameters with a clear semantic meaning.

def my_function(
    pos_param_1,
    optional_param_1="1",
    default_param_1=1,
):
    ...

my_function(
    pos_param_1="argument 1",
    optional_param_1="argument 2",
)

pros

con

Some exclusively use it at invocation time and declaration time.

def my_function(
    pos_param_1="",
    optional_param_1="1",
    default_param_1=1,
    new_param=None,
):
    ...

my_function(
    pos_param_1="argument 1",
    optional_param_1="argument 2",
)

pros

con

Takeaways

The code is certainly instructions for the machine but it is also the embodiment of a developer’s thoughts and approach to a solution. It is a message left by the author for the future. Whether it is during the code review process, the maintenance process or simply leveraging the existing modules. WE, the developers, (including the original author) will have to make sense of the code as fast and as unequivocally as possible the next time it is read. Our best-case scenario is to be able to use or to update the functionality built without having to read and understand each piece of code every time.

Additionally, depending on the programming language used, the semantic meaning of all variables can be used by the interpreter or compiler to optimize the code further.

Now, you are in the know.

Choose your path, do not settle