Can you defend your favorite programming language?

6 days ago 2

Every coffee corner with Software Engineers invites a healthy debate over “What’s the best programming language?“.

But many a time, we aren’t equipped with the right set of arguments, which mostly relies on programming features we have not contemplated.

Our day jobs tie us down to a particular language, framework or stack, so we hardly delve into the features of competing languages.

In this article, I have tried to explain a very common concept of programming language with contrast to multiple different languages that we as developers should know to have meaningful conversations, along with explaining them to any junior or beginner in tech.

I’ve not contrasted all the programming languages, but a handful of popular ones like Java, C++, Rust, Go, JavaScript, etc.

Chances are that you might already know a few of them, so here is a list of concepts I’ve discussed so you can skip the terms and jump directly to the topic.

  1. Garbage Collection
  2. Packages
  3. First-class functions
  4. Type system
  5. Asynchronous programming
  6. Scoping
  7. System Calls
  8. Immutability
  9. Generic programming
  10. Exception handling
  11. Programming Paradigm

As usual, in my articles, I’ve put a lot of markers for you to navigate the sections easily and save time. Here is a summary of common markers.

✅ – One-line definition

🤔 – Analogy to the real world

⚠️ – Limitations

👉 – Noteworthy

💰 – Advantages

🔗 – External Resource

🔴 – Pay attention

1. Garbage Collection

Photo by li hao on Unsplash

✅ Garbage collection is a feature in programming languages that cleans up the redundant and unused memory occupied by a running program.

🤔 Think of it like a cleaning robot in your program. When you’re done using something (like a variable), the robot throws it away for you so your memory doesn’t get full.

⚠️ GCs run with the program during runtime. This means that, along with your program, the GC runs and monitors the memory, performing cleanup operations. This leads to additional memory usage and can sometimes make programs slower.

For example, Java, through its JVM, includes a Garbage Collector component responsible for the automatic cleanup of memory.

The JVM’s GC utilises multiple algorithms and provides users with choices to select the most suitable one for their needs. It also automatically divides the heap memory into segments to manage runtime memory more efficiently.

You can read my post on LinkedIn explaining the JVM GC in depth.

👉 There are languages like C/C++ and Rust that do not employ a garbage collection mechanism.

Memory management in languages without GC is either handled manually by the programmer or through language-specific principles.

For example, take a look at this C++ snippet where the programmer is writing the memory management logic.

C++

int main() { int* arr = new int[5]; // memory allocated for (int i = 0; i < 5; ++i) { arr[i] = i * 10; } std::cout << "Array contents: "; // memory used for (int i = 0; i < 5; ++i) { std::cout << arr[i] << " "; } std::cout << std::endl; delete[] arr; // memory cleaned return 0; }

In contrast to this, in languages like Rust that do not employ even manual memory management features, the garbage collection concept is disposed of in favour of safe ownership.

Rust

fn main() { let boxed_int = Box::new(42); // memory allocated println!("Value: {}", boxed_int); // memory used // boxed_int is deallocated as it goes out of scope. }

💰 However, the biggest pro of having a garbage collector in any programming language is that developers don’t have to restrict themselves or work extra for memory management.

2. Packages

Photo by EJ Li on Unsplash

✅ A package is a way to group related artefacts like classes, functions, interfaces, etc together.

🤔 Think of it like putting similar items into a labelled toolbox so it’s easy to organise, find, and avoid conflicts.

❌ Packaging is a niche programming feature that is not available in all languages. The bundling of code and modularity can be achieved either with packages or with other techniques like file directories, modules, etc.

For example, Java has native package support, where you can assign an object to a package by declaring it at the top of the file.

Take a look at the code snippet below.

Java

package com.example.utils; public class MathUtils { public static int add(int a, int b) { return a + b; } }

Here, com.example.utils is a package that groups the classes, such as MathUtils.

Java

package com.example.moreutils; public class MathUtils { public static Integer add(Integer a, Float b) { Integer bToInt = (Integer) b; return boToInt + a; } }

👉 If I create another class MathUtils in a different package, it won’t affect the class in the first package. Hence, packages also help resolve namespace conflicts.

As I mentioned before, not all programming languages support packages, which makes it a significant point of discussion.

Now, C++ does not have a native concept of packages, but it uses namespaces, header files, and libraries to group related code.

💰 Having package support in a programming language helps with:

  • Avoiding name clashes — you can use the same class names in different packages.
  • Modular design and better code organisation.
  • Easy code import. For example, the JVM can implicitly import packages like Math and System during runtime.

TypeScript is another example of a language without native package support, but it uses modules and file structures to organise code.

3. First-class Functions

Photo by Mathias Reding on Unsplash

First-class functions are like values—you can store them in variables, pass them to other functions, or return them.

🤔 Imagine a function as a tool, and in languages with first-class functions, these tools can be carried around like items in a bag.

👉 First-class functions are a native concept in some programming languages, but ❌ not all languages support this feature in their syntax.

Take a look at the TypeScript snippet of first-class functions.

TypeScript

const greet = (name: string): string => { return `Hello, ${name}`; }; // Passing a function as an argument function callWithName(fn: (name: string) => string, name: string): string { return fn(name); } console.log(callWithName(greet, "Alice")); // Hello, Alice

Java does not have native or full support for first-class functions. It makes use of Functional Interfaces or Lambdas to mimic the first-class function use in the syntax.

Take a look at this snippet.

Java

// Functional interface interface Greeting { String sayHello(String name); } public class Main { public static void main(String[] args) { // Lambda as a first-class function Greeting greet = (name) -> "Hello, " + name; System.out.println(greet.sayHello("Alice")); } }

You can observe that line 9 gives the impression of first-class functions, but in reality, it’s just a functional interface Greeting declared on lines 2-4.

💰 The pro of first-class functions is the easy transferability by passing them as arguments and exporting them in the code to make them accessible globally in the project.

4. Type System

Photo by Edwin Petrus on Unsplash

✅ A type system in a programming language sets rules about how data types (number, string, text, etc.) work in the programs.

🤔 Imagine you’re at an airport, and you’re packing bags (variables) for a trip (your program). The airport has strict rules about what goes in which bag. The type system is like the airport security that checks if each item (value) is packed correctly according to its label (type).

👉 A programming language is categorised into two broad types:

Statically Typed

They require a strict declaration of the types for the variables being used. For example, in Java.

Java

void main() { String name = "Amrit"; // type declaration of 'name' variable }

For a statically typed language, the strictness can be more refined.

Now, there are languages like Rust where you have to define the exact type of the data type along with its size variant. These languages are then called Strongly Statically Typed languages.

Rust

fn main() { let x: i32 = 10; // signed 32 bit int let y: u32 = 5; // unsigned 32 bit int println!("{}", x + y); // Error: mismatched types }

Dynamically Typed

These do not require a declaration of the types for the variables being used. For example, in Python.

Python

def main(): name = "Amrit" # does not require type of the variable 'name'

The dynamically typed languages implicitly convert the data types to the required types during runtime.

Dynamic type is great for writing code faster, but can become a big heckle in a production system if the types do not match and cause a crash. This is the reason why Python has introduced features such as type hinting to avoid such scenarios.

⚠️ In general, dynamically typed languages can lead to more runtime errors and harder-to-maintain code due to the lack of compile-time type checking.

5. Asynchronous Programming

Photo by Jason Briscoe on Unsplash

In programming languages, asynchronous programming lets code initiate tasks that run separately, so the program can continue executing without blocking while waiting for those tasks to finish.

Imagine you’re cooking a meal with several dishes. Instead of waiting for one dish to finish before starting the next, you start cooking something, then while it’s simmering or baking, you move on to prep or cook other dishes.

A program usually executes step by step, going from one instruction to next but it doesn’t have to, the code can be executed asynchronously like muti-tasking.

Almost all programming languages now support asynchronous or concurrent programming. However, few languages do it more natively by baking concurrency directly into their code bases while other exposes it with well-defined APIs and Libraries.

Take this example of JavaScript asynchronous programming in Node.js runtime:

JavaScript

function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Data fetched'); }, 10000); }); } fetchData().then(data => console.log(data)); console.log('Prints first'); // prints before line 9

Here, the line 11 will run before the task is even finished on line 4. This happens because the runtime doesn’t wait for the execution completion on line 4 as it observes that the function fetchData is returning a promise.

For some languages asynchronous programming or concurrency is smoother and is part of native syntax construct for example Go and JavaScript.

For other languages like Java, more verbosity and API functions are required to perform the same task, for example, writing same code with Java CompletableFuture API will look like this.

Java

import java.util.concurrent.*; public class Example { public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } return "Data Fetched"; }); // Combine multiple asynchronous tasks future.thenAccept(result -> System.out.println(result)); // Do other work System.out.println("Prints first"); // prints before line 15 } }

Give or take almost all programming languages today support concurrency except few like C and COBOL. But few languages have better support through their language constructs and others don’t.

6. Scoping

Photo by Charles Chen on Unsplash

Scope is just the set of rules that decides where in your code a variable can be accessed or modified.

It’s the visibility map. If you’ve ever tried to use a variable outside of the block it was declared in, and your language yelled at you — that’s scope in action.

A language can support either type of scoping mentioned below.

Lexical Scoping

Lexical scope (also called static scope) is decided during the compile time. It’s determined by the structure of the code, for example, the way things are nested.

Most modern languages (like JavaScript, Python, Java, C++) use this and it’s easy to detect the scope of any construct by just looking at the curly braces and the variables and methods that are enclosed within them.

JavaScript

function outer() { let message = "Hello from outer"; function inner() { console.log(message); // Works! Because it's inside the outer scope. } inner(); }

In the above code, inner method has access to message variable, even though it wasn’t defined inside it. Why? Because it lives inside outer. It’s like being in a smaller room inside a bigger one — you can always peek outside.

We don’t want to go too much into the details, but lexical scoping provides multiple layers of access which are as follows.

Global Scope: Stuff which is defined globally and everyone can see it, access or change it depending how you declare them.

Function Scope: Things which are defined inside of function and every constructs within the function can access it but not the elements outside.

Block Scope: Even within a function you can facilities in few programming languages to restrict the visibility of elements within the function or class scope itself. For example open-close curly braces {} in Java.

Dynamic Scoping

Dynamic Scope is decided during runtime or by looking at the function call stack.

This scope is the realm of older and script-based programming languages like LISP, Bash, ABAP, etc. It is definitely not straightforward like seeing curly braces, and more unpredictable.

Bash

x=5 foo() { echo $x } bar() { local x=10 foo # prints 10 because foo was called *from* bar } bar

If you look at the code, the foo method won’t throw an error even though x is defined outside, because during runtime it picks the value of x when bar is called and puts it on the function stack.

This is more like asking “where I come from”, instead of, “where am I”.

7. System Calls

Photo by Egor Gordeev on Unsplash

✅ System calls are a feature of a programming language to directly communicate with the Operating System.

🤔 Think of your program as a customer at a restaurant, and the kitchen (which handles cooking, food storage, etc.) represents the operating system. But as a customer, you can’t go into the kitchen and cook your own food. You need to communicate through the waiter—this is your system call.

👉 Some languages allow you to directly talk to the OS level commands and interfaces, while others provide you APIs and Libraries to do so.

🔴 But why are system calls important?

Let’s say you’re writing some code — maybe reading a file, or printing something to the screen. On the surface, it looks pretty straightforward.

Python

with open("file.txt") as f: print(f.read())

❌ But here’s the thing: your code can’t just directly talk to the hard disk and start reading files.

There’s a whole world of rules and permissions and abstractions — and right in the middle of it all is the operating system.

Python can provide I/O operations that directly talk with OS level interfaces through its OS module, which can be considered as a Python wrapper on top of a low-level library written in C.

👉 However, there are languages like Go that provide it via its syscall package, which has much lower-level access to system calls supported in the operating system.

This gives Go an edge on performance when compared to a language like Python.

System calls are the gateway to real-world effects. It helps you to persist your results, retrieve them to have a permanent effect on the system.

💰 Some of the pros of having a system call native interface in a language are:

  1. Low-Level access: Closer to OS and low performance overhead.
  2. Clean abstraction over OS features: Consistent and native design over OS interfaces.

8. Immutability

Photo by Catia Climovich on Unsplash

Immutability means once a value is created, it cannot be changed. If you want a “different” version, you create a new value instead.

Imagine writing a note with ink on paper. Once you’ve written it, you can’t erase or change it—you can only write a new note if you want to say something different.

The best example to test immutability in any programming language is to look at how it handle strings.

Python

name = "Alice" name[0] = "M" # This throws an error

In Python, you cannot mutate the string once it’s assigned to a value, you can only reassign to a new value.

However, you can achieve the same in C++.

C++

int main() { string name = "Alice"; name[0] = 'M'; // works return 0; }

Immutablity can be a cause of headache. For example, in JavaScript you can do something like this.

JavaScript

let arr = [1, 2, 3]; let other = arr; other.push(4); console.log(arr); // arr is now [1, 2, 3, 4]

The can be expanded to include unwanted elements like ‘4’. We probably will have to write more code to prevent any mishap that might happen by such a feature.

9. Generic Programming

Photo by Alexey Demidov on Unsplash

✅ Generic programming refers to the same thing as it’s same suggest: programming for generic use cases or data types.

🤔 Think of generic programming like a universal remote control. Instead of having separate remotes for your TV, sound system, and AC, you have one remote that can control any device, as long as you tell it what kind it’s working with.

When writing an algorithm, we may think of solving for a particular data type, for suppose, it can be either strings, numbers or boolean.

❓But what if I want to write a common algorithm that can be applied to any data type, and this data type can be specified later by the user or programmer who is going to use my algorithm?

👉 Generics are a common term that is used in exchange with “template“, i.e. I write a template algorithm in which the data type is filled later.

💰 The advantages of Generics are as follows:

  1. Resusability: We can reuse the same logic for different types.
  2. Abstraction: We focus more on business logic and operations on the data without fine-tuning them for each individual type.
  3. Type Independence: Works for every type.

Here is an example of generics in Java.

Java

class Example { public static void main(String[] args) { ListNode<String> list1 = new ListNode<>("One"); // list node of type String ListNode<Integer> list2 = new ListNode<>(1); // list node of type Integer } // <T> generic type parameter public static class ListNode<T> { public T val; public ListNode<T> next; public ListNode(T item) { this.val = item; } } }

Here, the<T> is a generic type parameter that is inferred during compile time according to the type being used. With this I can create LinkedList of any type, i.e. String, Integer etc., without changing the underlying logic.

❌ Dynamically typed languages obviously do not provide Generic support. However, few old typed languages like C or Objective-C have no to very poor support for generics.

Almost all typed languages that are used at scale have support for Generics, viz. Java, Go, Rust, C++, etc.

10. Exceptions

Photo by Jamie Street on Unsplash

Exceptions are runtime entities in the program that indicate errors in the program and gracefully handle execution flow instead of crashing the program.

🤔 Imagine you’re driving with a GPS guiding you to your destination (your program’s goal). Suddenly, a road is closed due to construction. Instead of crashing your car or stopping abruptly, the GPS alerts you and reroutes your path – that’s an exception!

Exceptions are more of an honoured way of handling errors in a program.

What do I mean by “honoured“?

👉 Well, if your program throws an error because you made a divide by zero mistake, then instead of crashing your program and giving a segment fault or fatal error, it gracefully gives you an object containing information on error and stack trace(series of code execution that led to this error) which can help you rectify the problem in the code.

💰 Errors are an inevitable phenomenon in a program however, Exceptions can be defined by the programmers themselves. Exception gives programmers control over the parts of code which can become errornous at some point of execution.

Additionally, exception can also be used as a branching mechasim to circumvent the error causing code.

For example, a programmer can design the code in such a manner that when there is timeout from the server and an exception is thrown, they can retry the request again from the place where the exceptions are thrown.

Here’s a simple example in Java.

Java

class Example { public static void main(String[] args) { int a = 10; int b = 0; Scanner sc = new Scanner(System.in); try { int result = a / b; } catch (ArithmeticException e) { System.out.println("Error: Cannot divide by zero!"); e.printStackTrace(); } finally { // branching to continue execution flow System.out.println("Pick a non-zero number"); b = sc.nextInt(); // continue } } }

❌ The concept of exception has become very popular however there are still few new languages which do not implement them. Popular examples include Go and Rust programming languages.

11. Programming Paradigms

Photo by Etienne Girardet on Unsplash

✅ Paradigms in programming are just different ways of writing code.

🤔 Imagine you’re building a house. There are different styles of construction, like using bricks, wood frames, or prefabricated materials. Each method has its own rules, tools, and best use cases. Similarly, programming paradigms are different styles or approaches to building software.

Each paradigm will have its own rules, syntax constructs and different ways of solving problems.

⚠️ Since the dawn of programming, developers have invented many paradigms, many of which are used on a great scale, and many have gone extinct.

I won’t go deeper into what each paradigm entails, but I will explain the two most popular ones.

Imperative/Procedural Programming

It is the simplest programming paradigm out there. It relies on code being written in a step-by-step manner, much like a recipe.

The organisation of code at max takes place in procedures or functions that are blocks of code which has to be repeated at multiple steps.

The common constructs that are included contain loops, conditionals, procedures and data types. And they are all used linearly, much like a flow chart.

For example, take this C code.

Java

#include <stdio.h> const char* message = "Hello"; int main() { sayHello(); // 1 sayHelloAgain(); // 2 for (int i = 0; i < 10; i++) { sayHello(); // 3 if (i == 5) { break; } } return 0; // 4 } void sayHello() { printf("%s !\n", message); } void sayHelloAgain() { printf("Hey there!\n"); }

As you can observe, this code calls the procedures or methods step-by-step.

Object-Oriented Programming

This is the paradigm where the programmer has to organise their code in objects or containers.

It may seem counterintuitive to organise something that has to be executed in containers, but think of it like creating living, breathing entities of code which has two things:

  1. Properties: Data attributes of the object.
  2. Behaviour: Logic that can operate on its stored and external properties.

Take a look at this Java code.

Java

class Hello { final String message = "Hello"; public static void main(String[] args) { sayHello(); sayHelloAgain(); } private void sayHello() { System.out.println(message + "!"); } private void sayHelloAgain() { System.out.println("Hey, there!"); } }

Hello is an object that contains properties (message) and behaviour (sayHello, sayHelloAgain) methods.

OOP introduces a lot many other rules, such as how and when the code within a container can be used. Also, it answers questions like Can code be reused? And who can reuse the code?

All of these are done with OOP concepts like Encapsulation, Inheritance, Polymorphism, and Abstraction.

Just like these two, you can also explore other popular paradigms: Functional, Declarative, Event-Driven/Reactive, Aspect-Oriented, Symbolic, etc.

🔴 Now let’s get to a very important question: Are programming paradigms a feature or a programming language?

⚠️ The answer to this question is both “yes” and “no”.

In the simplest sense, many programming languages were designed keeping a particular paradigm in mind, and others inherited the paradigms from their predecessor language.

For example, Java is strictly object-oriented language. Python is an object-based language, hence, it can never escape OOP itself.

👉 However, not all programming languages adhere strictly to a paradigm, and thus paradigms cannot always be bottlenecks.

For example, C supports an imperative paradigm however, with structs in C we can achieve programming similar to OOP.

Same goes for JavaScript, which is a prototypal language, however, its language construct can support many paradigms such as procedural, OOP, functional, etc.

The choice of language can also lead the developer to accept the particular paradigm, which we see in the case of Go and Java.

The End

So that’s a wrap!

I hope your programming conversations with colleagues and folks become more interesting now.

And, lastly, I want to ask you … what’s your favourite programming language?

Subscribe to my newsletter today!

Read Entire Article