The Curse of Verbosity and Indirection

I've dealt with a wide spectrum of programming languages over the years. Some languages are object-oriented (e.g., Java, PHP, C++, and Dart); some are procedural (e.g., C and Zig); some are functional (e.g., Haskell, Coq); some are declarative (e.g., HTML and CSS); some are a mixed bag of everything (e.g., JavaScript, Python, Rust); and some are straight-up machine code (e.g., x86, ARM, MIPS, and RISC-V). One common theme that I've observed is that object-oriented programming (OOP) patterns tend to yield the most verbose and indirect abstractions versus their non-OOP counterparts. In this article, we'll investigate why this is the case and how it arises out of necessity. Free functions as a first-class citizen package com.example.hello; public class Main { public static void main(String[] _args) { System.out.println("Hello world!"); } } fn main() { println!("Hello world!"); } Let's compare two "hello world" programs: one from Java and another from Rust. It wouldn't be controversial to assert that the Rust version is clearly more concise than the Java version. To be fair, there is a lot more magic abstracted away: the command-line arguments being the most notable omission. Putting that aside, this example illustrates the key limitation of "pure" OOP that makes it so verbose: classes are the only first-class citizens at the top level. Functions, on the other hand, are second-class citizens that must belong to either a class (as a static method) or an object (as an instance method). When classes are imposed as the top-level entity, class declaration boilerplate becomes a necessary syntactic and semantic overhead. Instead of defining the entry point as a free function (as one typically would at the assembly level), a strictly OOP language introduces a class indirection right from the start. The Zig language—at least as of writing—brilliantly compromises on that mental model by treating files as implicit structs. Top-level items in a file would be as if everything had been wrapped by an invisible struct { ... } declaration. // example.zig // struct { // Not exported... const std = @import("std"); // Exported as a static method! pub fn hello() void { std.debug.print("world"); } // } // main.zig // struct { const example = @import("./example.zig"); pub fn main() void { // Static method! example.hello(); // world } // } Interestingly, this is somewhat equivalent to the Java module system. The only difference is that the class boilerplate is implied, which makes things a little bit more bearable (as far as "hello world" examples go).

Mar 23, 2025 - 18:42
 0
The Curse of Verbosity and Indirection

I've dealt with a wide spectrum of programming languages over the years. Some languages are object-oriented (e.g., Java, PHP, C++, and Dart); some are procedural (e.g., C and Zig); some are functional (e.g., Haskell, Coq); some are declarative (e.g., HTML and CSS); some are a mixed bag of everything (e.g., JavaScript, Python, Rust); and some are straight-up machine code (e.g., x86, ARM, MIPS, and RISC-V).

One common theme that I've observed is that object-oriented programming (OOP) patterns tend to yield the most verbose and indirect abstractions versus their non-OOP counterparts. In this article, we'll investigate why this is the case and how it arises out of necessity.

Free functions as a first-class citizen

package com.example.hello;
public class Main {
    public static void main(String[] _args) {
        System.out.println("Hello world!");
    }
}
fn main() {
    println!("Hello world!");
}

Let's compare two "hello world" programs: one from Java and another from Rust. It wouldn't be controversial to assert that the Rust version is clearly more concise than the Java version. To be fair, there is a lot more magic abstracted away: the command-line arguments being the most notable omission.

Putting that aside, this example illustrates the key limitation of "pure" OOP that makes it so verbose: classes are the only first-class citizens at the top level. Functions, on the other hand, are second-class citizens that must belong to either a class (as a static method) or an object (as an instance method).

When classes are imposed as the top-level entity, class declaration boilerplate becomes a necessary syntactic and semantic overhead. Instead of defining the entry point as a free function (as one typically would at the assembly level), a strictly OOP language introduces a class indirection right from the start.

The Zig language—at least as of writing—brilliantly compromises on that mental model by treating files as implicit structs. Top-level items in a file would be as if everything had been wrapped by an invisible struct { ... } declaration.

// example.zig
// struct {

// Not exported...
const std = @import("std");

// Exported as a static method!
pub fn hello() void {
    std.debug.print("world");
}

// }
// main.zig
// struct {

const example = @import("./example.zig");

pub fn main() void {
    // Static method!
    example.hello(); // world
}

// }

Interestingly, this is somewhat equivalent to the Java module system. The only difference is that the class boilerplate is implied, which makes things a little bit more bearable (as far as "hello world" examples go).