Friday, February 23, 2024

The pitfalls of pure rationality

Being rational with regards to the surrounding natural and physical world is undeniably good for our survival. We learn the effects of touching a hot stove and never attempt to do it consciously once we are older than 4. We know that driving on ice makes a vehicle hardly controllable, and tend to avoid it. This cause and effect paradigm is incredibly helpful for navigating the natural world. We don't need constant empirical proof for stuff like that. We don't need to deconstruct any social constructs.

However, I believe that this paradigm, when applied abstractly to human-made institutions and enterprises, skews our social understanding and navigation abilities. It fabricates toxic conventional wisdom and explanatory models that supress basic reasoning in a local, context-sensitive manner. The Scottish philosopher David Hume famously said that "reason is a slave of the passions". Once you start viewing the world through deterministic mega power structures and institutions, you are no longer sensitive to your local environment. You stop seeing a window of possibilities and the particular qualities of the people around you. Many of these assumptions are time- and region- sensitive and can become even less relevant with time.

If you become overly hooked on formalisms like behavioural economics, cognitive psychology, evolutionism or the geopolitical power struggles, you are no longer an ingenious free-acting agent. You attempt to explain the behaviour of your neighbour with theories developed by distant academics. You start taking decisions symbolically and in a virtue-signalling manner – "I will buy from another brand because I believe that someone I don't know 5000 miles away is a creationist".

What would the cure be? Develop social intuitions. Observe your local surrounding environment and the people around you. Try to listen to their personal stories. Tell them yours. Then you can probably figure out what's valuable for them, what drives them every day. Applying that knowledge in the economic realm might change the world for the better.

Thursday, December 14, 2023

The Essence of "Modern C++"

I can remember my first teenage attempts at learning programming in an imperative OO style with C++ in the late 1990s. That was the pre-stackoverflow, pre-Web 2.0 era when the web generally consisted of a few keyword-based search engines and millions of weird personal pages with those nasty "blink" tags. Any educational material covering my interest in programming was sparse and hard to come by. Living in a poor Eastern European post-communist country that was still pretty much isolated from the Western world and not knowing any conversational English didn't help, either. My best bet were a couple of locally-available, badly-translated C++ books that were mediocre to begin with. Having access to anything from Scott Meyers in English was probably a privilege for the very few.

The common ethos of those books revolved around inheritance, virtual functions and operator overloading. They never showed you how to approach a real programming problem. I was eager to learn how to create a basic game with moving stuff on the screen, but all I got was dead-boring hierarchies of shapes, animals and employees with short, mostly one-liner methods. Defining your own Matrix class, overloading the "+" operator and doing "m1 + m2" was advertised as cool and exciting. If there was a minimal, quintessential example I absorbed, it was this:

class Shape {
public:
    virtual float area() = 0;
};

class Square: public Shape {
private:
    float side;

public:
    Square(float side): side(side) {};

    float area() {
        return side * side;
    }
};

class Rectangle: public Shape {
private:
    float width, height;

public:
    Rectangle(float width, float height): width(width), height(height) {}

    float area() {
        return width * height;
    }
};

Then they showed you how to instantiate and use these hierarchies:

Square *sq = new Square(5);
Rectangle *rect = new Rectangle(3, 4);

vector<Shape *> shapes = vector<Shape *>();
shapes.push_back(sq);
shapes.push_back(rect);

for (int i = 0; i < shapes.size(); i++) {
cout << shapes[i]->area() << endl;
}

shapes.clear();
delete sq;
delete rect;

And that was mostly it. At best, you learned how to implement a linked list. No Pong game or flying teapots on the screen. At that point, continuing playing with Lego looked more interesting and certainly more joyful than programming. There was never a word about any potential problems with that style of programming. Segfaults, leaks, dangling pointers, reading uninitialised data, overflows - there was the implicit assumption that good programmers don't make such mistakes. There was never the concept of "object ownership" as it is today. Passing objects between functions was an exercise of ad-hoc trickery. You had to learn the hard stuff by playing with the weird behaviours yourself.

Now, I would define the essence of modern C++ with transforming the above fragment into:

auto shapes = std::vector<std::unique_ptr<Shape>>();
shapes.push_back(std::make_unique<Square>(5));
shapes.push_back(std::make_unique<Rectangle>(3, 4));

for (const auto& shape: shapes) {
    cout << shape->area() << endl;
}

This code cannot leak and is exception-safe. The "pointers" are value objects with enforced semantics. Behind the scenes, move semantics transfers the ownership of the unique pointers from their temporary expressions to the containing vector. When the vector goes out of scope, everything else disappears safely.

Thursday, August 24, 2023

The trap of Unix

Ever since the initial implementations of Unix at Bell Labs in the early 1970s, we are stuck with some basic design decisions that have persisted in the programming interfaces of our operating systems for decades. To name a few - a process hierarchy based on a parent-child relationship between processes, a hierarchical filesystem with an embedded permission model, IO as a stream of bytes, programs stored as yet another binary file, the notion of a controlling terminal attached to user processes, the notion of a "shell" that mediates an interactive session between a terminal and the user.

While those decisions may have been very adequate in the "time-sharing", "minicomputer" era of PDP-11, I think we are long past their potential. In 2023, we build and use complex systems solving completely new high-level problems, using these primitives as the basic building blocks, without ever questioning their existence. How can we do better by rethinking those primitives?

The parent-child process hierarchy

In a typical Unix system, the process hierarchy is made up of independent processes having a parent-child relationship between them. The parent needs to wait for the termination of its direct children in order to obtain their exit code and release their PID for reuse.

In the image above, the init process has a number of getty processes and daemons as its direct children. Each getty process listens for an incoming connection from a particular terminal, identified by a filename. Once a terminal connection is established, the getty process prompts the user for their username, and invokes the /bin/login program with the entered username as an argument. This invocation is done in-place using the exec() system call, which replaces the whole program image and preserves the place of the process in the hierarchy, retaining the PID. Then, in turn, /bin/login prompts for the user's password and if it is correct, starts the /bin/sh program, again using exec().

Now, the shell process has a "controlling terminal" (for example, /dev/tty2) and will handle the interactive session with the user. The user would type a command, the shell would interpret it and start a sub-hierarchy of child processes with the same controlling terminal, waiting for their termination. The beauty of this approach is that for the user programs (cat and grep in the example), the input and output is done via the standard 0 and 1 file descriptors, with standard system calls. Those programs normally don't care about the specificity of the particular terminal they are writing to - its driver is implemented in the kernel.

There are also "daemon" programs, distinguished by the fact that they are not attached to a terminal, and having their 0, 1 and 2 descriptors closed or redirected to files.

You can now see that the whole idea of this hierarchy revolves around the way this system was used in the 1970s. Multiple users would log into the system from various terminals. They would start small programs via the shell, and those small programs would interact with the terminal seamlessly. Had there been a "flat" process arrangement, this wouldn't be so easy to achieve - individual user processes would have to "open" the active terminal in one way or another.

The question is, can we do with a simpler process hierarchy, now that physical terminals are long gone? What if every process was independently started and terminated, uniquely identified by non-reusable system-wide identifiers, handed to individual processes based on some security policy? Then we wouldn't need to wait() for their termination, or worry about "zombie processes".

Hierarchical filesystem

The primary metaphor for a hierarchical filesystem is that of a file cabinet with sorted folders inside. While this can be very convenient for some kinds of files - for example, user documents, photos or songs, it can cause a lot of headaches when you start putting binary programs and their configuration state as text files in that same global tree. Things break because stuff in that tree often isn't found or is in an undesired state.

We could still retain that tree structure for what it's good at, but we could also implement a binary versioning mechanism at the lowest level in the kernel, without relying on fragile package/container/whatever managers dealing with the complexity of the file tree. Programs, libraries and their configuration state can be managed via global identifiers and version numbers, and a corresponding system API.

IO as a (blocking) stream of bytes

The default mode of operation for Unix programs is to block while waiting for input data from the terminal or from a file, and ... to block while writing data to the terminal or to a file. To overcome this, a number of APIs have appeared since the select() system call.

What if programs are just long-living services responding to events from the kernel, with their own registered callbacks? The kernel would wake the service with some structured data ready for consumption. We wouldn't need to do so many trips and context switches around a "file descriptor".