Position or Precision? How Passing Arguments Evolved in Programming Languages
Whenever you started writing programs, in the 60s, 90s, or last year, you probably encountered problems with code organization. You probably thought about passing the whole object as an argument instead of 10 simple ones, or you had typed battles in a world of dynamically interpreted languages. Anyhow, I will show you through a timeline in history how we defined parameters, how we overcame obstacles by introducing new ideas, and how we should think about parameters.
We won’t cover how value is passed (by reference or by value); we will focus on syntax and readability.
1940s - Rise of Assembly Language
Unfortunately ugly, but fortunately for the first time in history, we can write machine code that both we and machines could interpret.
A simple add function would look like this (I know ‘add’ function exists in assembly, this is for the sake of example):
add_two_nums:
mov eax, ecx ; First arg (ecx) -> eax register
add eax, edx ; Second arg (edx) -> eax register
ret
; Main program
main:
; Set up arguments
mov ecx, 5 ; First arg = 5 (in ecx)
mov edx, 7 ; Second arg = 7 (in edx)
; Call the function
call add_two_nums ; Result is saved into eaxPassing parameters in assembly revolves around putting values into the right registers. We can’t put default values, we don’t have a function definition, so we don’t even know how many args we can pass.
1960s - B lang, C lang
Thanks to Ken Thompson and Dennis Richie, our languages evolved into higher abstractions, where we can finally use positional arguments!
Simple function now looks like this:
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3); // Passing arguments
...
}We see how many parameters functions have, their types, and their order. But this example is primitive.
Imagine that we are writing a user service, and we have to write a function that creates a user like this:
int create_user(
const char *email, // Required
const char *first_name, // Required
const char *last_name, // Required
const char *nickname, // Optional (pass NULL if unused)
int age, // Optional (pass -1 if unused)
bool is_verified // Optional (default: false)
) {
...
}Then we should be careful how we call this function because we can easily swap parameters by mistake:
create_user("john.doe@example.com", "John", "Doe", NULL, -1, false); // Name is John
create_user("john.doe@example.com", "Doe", "John", NULL, -1, false); // Name is Doe?Another confusing part lies around the idea of optional arguments. Should we pass them, or can we pass age without sending a nickname in the example above?
But C programmers were clever, and for these types of issues, they started using a struct as a way to pass arguments:
// Struct to hold user data (named arguments)
typedef struct {
const char *email;
const char *first_name;
const char *last_name;
const char *nickname; // Optional
int age; // Optional (default = 0)
bool is_verified; // Optional (default = false)
} UserParams;
// Default values
#define USER_DEFAULTS { \
.nickname = NULL, \
.age = 0, \
.is_verified = false \
}Using a struct as a parameter made function calls readable more than ever. We can omit default values, we don’t have to care about order, but this is more of a hack (although JS guys use props object in a similar vain) because this isn’t built into language syntax:
int main() {
create_user((UserParams){
.email = "john.doe@example.com",
.first_name = "John",
.last_name = "Doe"
});1980s - Revolution: When We Start Caring About Readability
Ada was revolutionary in 1983 when they introduced named parameters as shown below:
procedure Create_User (
Email : in String;
First_Name : in String;
Last_Name : in String;
Nickname : in String := "";
Age : in Natural := 0;
Is_Verified : in Boolean := False
) is
begin
-- method implementation
end Create_User;
-- Positional call
Create_User("john@example.com", "John", "Doe", "Johnny", 30, True);
-- Named parameters (in any order!)
Create_User(
First_Name => "Jane",
Email => "jane@example.com",
Last_Name => "Smith",
Is_Verified => True
);Unfortunately, Ada didn’t succeed and became a popular language due to the industry at the time, but it showed us why clarity and readability are important.
Common Lisp in 1984 also made parameters a first-class feature, as you can see:
(defun create-user-positional (email first-name last-name nickname age is-verified)
...)
; Calling positional
(create-user-positional "john@example.com" "John" "Doe" "Johnny" 30 t)
(defun create-user-keyed (&key email
first-name
Last-name
(nickname nil)
(age 0)
(is-verified nil))
...)
; Calling keyed
(create-user-keyed
:first-name "Jane"
:email "jane@example.com"
:last-name "Smith")Unfortunate like Ada, CLISP didn’t succeed in the programming industry, but every now and then new Lisp dialect shows up, like Clojure in 2007, with some old ideas redefined into something new, which helps us to think differently about programming, but let’s stay focused on the main topic of this blog.
1990s - The Mainstream Adoption
In 1991, Guido van Rossum continued in the same vein as Ada and Common Lisp, allowing functions to be called either by passing arguments positionally or by using named parameters:
def create_user(
email,
first_name,
last_name,
nickname=None,
age=0,
is_verified=False
):
# (Implementation would go here)
pass
positional = create_user(
"john@example.com",
"John",
"Doe",
"Johnny",
30,
True
)
keyed = create_user(
first_name="Jane",
email="jane@example.com",
last_name="Smith",
nickname="J"
)The same idea is implemented in Scala:
def createUser(
email: String,
firstName: String,
lastName: String,
nickname: Option[String] = None,
age: Option[Int] = None,
isVerified: Boolean = false
): Unit = ???
val positional = createUser("john@example.com", "John", "Doe", "Johnny", 30, true)
val keyed = createUser(
firstName = "Jane",
email = "jane@example.com",
lastName = "Smith"
)Python became one of the most popular languages, and Scala and Kotlin’s modern language features forced Java to implement similar features in fear of not losing competition in the JVM world. Modern languages like Rust, Kotlin, Elixir, and Nim continue this trajectory.
The Evolution Continues
In JavaScript, you can use the destructuring pattern to extract only what you need from the passed object:
function createUser({email, firstName, lastName, ...options}) {
// Extract what you need, ignore the rest
}One Last Argument Against Positional Arguments:
// Do we know how the server is configured?
configure_server(8080, 100, 30, True, 5000)
// Now we know
configure_server(
port=8080,
max_connections=100,
timeout_seconds=30,
enable_ssl=True,
enable_compression=False,
buffer_size=5000
)While positional arguments are still necessary in low-level programming, we can see a clear trend: as complexity grows, readability becomes increasingly important, if not the most important aspect.
Using an unambiguous way to pass arguments will probably save you, if not days, then hours when you debug or change your code six months from now.