Languages That Expose the Details

When most people start programming, they're drawn to languages that make things easy. Python, JavaScript, and other high-level languages abstract away the messy details of memory management, system calls, and hardware interaction. This abstraction is powerful—it lets beginners create useful programs quickly without getting bogged down in implementation details.

But there's significant value in languages that force you to confront these details. Languages like Rust, C, and Zig don't just make you a better programmer in those specific languages—they deepen your understanding of how computers actually work. This understanding makes you more effective in every language you use, even the high-level ones.

To demonstrate, let’s take a “simple” concept like reading input from the user and storing it in a variable, then demonstrate how it would be done from higher to lower-level languages. We’ll start with the highest of them all:

Python

name = input("What is your name?\n")
print(name) #Ah, the classic I/O example

To a learner, what could the questions and learning be here? Remember, we aren’t just trying to crank out code, but to actually have an idea of what’s going on:

These aren’t bad; I think the biggest knowledge about computers will come from that little ‘\n’ in the string. Exploring a little on this would lead to knowledge about ASCII, UTF-8, and the representation of text in the computer as bytes. It would probably be too much for a beginner, but it would give them an idea of how text goes to 0s and 1s. There is also a little lesson on interpreters and compilers here, but that would require significant digging.

Javascript/Typescript (Node)

import readline from 'readline/promises';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const name = await rl.question('What is your name?\n');
  rl.close();

console.log(name);
//Oh, js, so horribly wonderful in your ways

In addition to the previous insights, let’s evaluate what a curious learner could observe from simply exploring this code:

Some of these, like Promises and Queues, are JS-related abstractions at first glance, but if a person eventually finds their way to Libuv — the C library that handles asynchronous I/O for Node.js - they’re going to learn a little about I/O in operating systems.

C-Sharp

Console.WriteLine("What is your name?");
string? name = Console.ReadLine();
Console.WriteLine(name); //Surprise!! No public static void Main(string[] args)

The usual suspects of character encoding are here, albeit obscured by the ReadLine and WriteLine, besides those, two important things come up:

Forcing a user to compile their code allows them to see the generated IL. This allows us our first look into assembly(pseudo-assembly, anyway), instructions, and registers. There is also the potential to learn about Just-In-Time compilation of the CLR if a learner pokes a little further under the hood.

While these concepts do exist in other languages, the difference is that exposing them to the user allows them to immediately poke deeper and gain an idea about what really happens to run the code.

Finally, I/O Is More Abstracted Here than in JS. We don’t have anything related to the streams and resource management.

Golang

Sorry, Gophers, but I can’t cover everything, or this article would become too long.

Rust

use std::io;

fn main() {
    println!("What is your name?");
    let mut name = String::new();

    io::stdin()
        .read_line(&mut name)
        .expect("Failed to read line");

    println!("{}", name);
}
//Almost a 1:1 from The Book

To a moderately curious learner, what could be understood about system concepts:

Even if you’re comfortable with high-level abstractions, experimenting with one small element in Rust can illuminate a whole world of system behavior that remains hidden in other languages. Most of these are not new, so to say, the difference is that here, they are exposed to the programmer, forcing them to think and learn about them. This does bring extra overhead and difficulty, but this is rewarded by a deeper understanding, and consequently, power over the resources of the system.

Zig

const std = @import("std");

pub fn main() !void {
    var debugAllocator = std.heap.DebugAllocator(.{}).init;
    defer std.debug.assert(debugAllocator.deinit() == .ok);

    const allocator = debugAllocator.allocator();

    const stdout = std.io.getStdOut().writer();
    const stdin = std.io.getStdIn().reader();

    var name = std.ArrayList(u8).init(allocator);
    defer name.deinit();

    try stdout.print("What is your name?\n", .{});
    try stdin.streamUntilDelimiter(name.writer(), '\n', null);

    try stdout.print("{s}\n", .{name.items});
}
//lol, the code block doesn't have support for Zig

NOTE: I really debated whether or not to include heap-allocated ‘growable’ strings or to simply have a very large, stack-allocated ‘static’ string, but since I’ve used ‘growable’ strings for every other example, here we are. To explain simply, a growable string can grow with extra input, whereas a static string is fixed — the only way to add new characters is to create a new one with the new character.

Oh boy, where do we start? If a learner were to look at this code, they’d probably get scared and run away, but what could be learned about system concepts by exploring this:

C and C++

I think you get the point I’m making here; no need to beat a dead horse.

So, there you go. A simple task in multiple languages. I hope you’ve been convinced of my point. Before we go, though, let’s make a few things clear:

So, Just Write Rewrite It In Rust?

No, the answer is no — just no. I make these arguments from the perspective of one who wants to learn more about how computers work and the one who needs to squeeze everything out of your hardware (you could also go assembly, like the guys over at FFMPEG for that, if you want).

But what happens when you’re fine with sacrificing some efficiency for the sake of development speed? What if you need to write a lightweight web server with some logic in a day or two? What happens when you’re a new developer who’s so intimidated by C++ that they want to drop code?

There are a lot of situations out there for which something like Go, Elixir, Haskell, or whatever is just fine. I only ask that after that, you take some time and learn a little bit about what’s really going on; you don’t have to be a low-level whiz who can write asm in their sleep, but knowing what happens with computers will help you write better, more performant code. And it’ll help you stop seeing your computer as a black box. I also promise you’ll enjoy it.

Talk to me on Twitter.