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 GIPHYA new understanding from the ground up in 2s
Kinds of parameters
The kinds of parameters discussed in this post are:
- positional parameter: the parameter expects the argument to be given at the matching position
- keyword parameter: the parameter expects the argument to be given using the matching key
- optional/default parameter: the parameter expects the argument to be given using the matching key or omitted
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 GIPHYSemantic 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:
-
pos_param_1
: this parameter is required and its placement in the sequence in the parameter’s list is likely important -
kw_param_1
: this parameter is required and its placement in the sequence in the parameter’s list is meaningless -
optional_param_1
: this parameter is optional, its placement in the sequence in the parameter’s list is meaningless and the function will work just as well if it is not supplied -
default_param_1
: this parameter is optional, its placement in the sequence in the parameter’s list is meaningless and the function will work just as well if it is not supplied
Note
- the optional and default parameter are semantically identical. i.e. to the reader of the code, they convey the same information and they behave the same way
- semantically, whether the optional and default parameter are defaulted to
None
or a different value (here respectively"1"
and1
) is identical.None
is a value and as such, it does not convey a “requirement”. i.e. that the default of a parameter isNone
does not mean that the parameter is required and needs a non-None
value. It means ”None
is one of the values I can have, all good”.
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:
-
kw_param_1
: this parameter is required and its placement in the sequence in the parameter’s list is meaningless -
kw_param_2
: this parameter is required and its placement in the sequence in the parameter’s list is meaningless -
optional_param_1
: this parameter is optional and the function will work just as well if it is not supplied -
optional_param_2
: this parameter is optional and the function will work just as well if it is not supplied
Note
-
optional_param_1
andoptional_param_2
are 2 ways of expressing the same thing. -
kw_param_2
andoptional_param_2
express different intent: omitted/undefined
means “I will work just as well if it is not supplied”,null
means “I need a value and the value may benull
which dictates a specific behaviour”.
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 fromfirst_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 fromfirst_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:
-
user_1
explicitly has no avatar. Deal with it! -
user_2
explicitly has an avatar. Easy, let’s use it! -
user_3
, it is unclear. They may or may not have one, it was just not given. Defensive programming, here we are! Is it a 3 state value (null
,string
,undefined
)? or isnull
andundefined
the same?
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
- it is easier to add a parameter:
- the task of ordering arguments/parameters is no longer applicable
- it is easier to remember the name (and meaning, if properly named) of arguments/parameters.
con
- the arguments lose most semantic meaning: without reading the declaration of the function you cannot tell if an argument is needed or not: it hard to distinguish a bug (missing argument) from an intended declaration
- you still need to add a new positional argument for all invocation
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
- easier to add a parameter:
- you do not need to add the argument to all invocations
- the task of ordering arguments/parameters is no longer applicable
con
- it is hard (if at all possible) to set sensible defaults
- the code in the function needs to handle the possible combinations (permutations do not matter) of supplied vs not supplied arguments: lead to lots of defensive programming or hoping for the best and dealing with consequences
- the arguments lose all semantic meaning: without reading the body of the function you cannot tell if an argument is required or not
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