upvote
My journey is quite similar. My mental model got a huge boost after I read and understood Leslie Lamports early work and the work of Edward Lee about getting deterministic results in the presence of concurrency. I even found the earliest paper with a mathematical proof that write and read must be separated in time or space (the math basics of the rust borrow checker), but don't find it anymore.

- https://lamport.azurewebsites.net/pubs/time-clocks.pdf

- https://en.wikipedia.org/wiki/Chandy%E2%80%93Lamport_algorit...

- https://www2.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-...

reply
Yeah, passing by value or "Value semantics" can prevent many programming errors. Passing references to immutable data can serve a similar purpose. In low-level languages where memory layout and calling convention map to target hardware, there are differences in performance to consider.

Pass by value would indeed make a big difference to how programs are structured and make it easier to reason about programs.

I just want to point out that "concurrency safety" is very much a word, although "thread safety" is more common. These are broadly part of memory safety, which is a topic mainly due to security concerns but also academic study.

The two perspectives are not perfectly congruent. Non-concurrency-safe languages like go can also be considered broadly memory safe. The pragmatic rationale is that data races in GCed languages are much less exploitable. From a academic, principle based view this is unsatisfying and unconvincing as one would prefer safety to be matter of semantics. See also https://www.ralfj.de/blog/2025/07/24/memory-safety.html

Rust uses "fearless concurrency" as a slogan. Rust offers more options than passing by value (Copy) while still guaranteeing safety through static type checking.

There is also research for GCed languages to establish non-interference eg Scala capture checking.

Concurrency is recognized as difficult (at least by people who are knowledgable) and programs language design usually involves pragmatic choices if you need concurrency. If the language does not provide the primitives or spec that enables safety, then you are left with patterns and architecture.

The science is still evolving, it is certainly not the case that nobody cares. Rather, progress is slow and moving ideas from research industry is even slower. How much value we ascribe to correctness, safety and performance in industry depends very much on the context.

reply
> only messages should move between objects

Can you provide an example for this?

reply
The Alan Kay viewpoint (he is NOT the inventor of OOP [1]) is considered the least helpful viewpoint on OO design. The “magical” and unhelpful “its all about messages” perspective, that helps you not at all unless one is talking about the internal implementation of a platform like Smalltalk. Consider the views of the real inventors - Nygaard and Dahl.

[1] I don't think I invented "Object-oriented" but more or less "noticed" what was really powerful about just making everything from complete computers communicating with non-command messages. This was all chronicled in the HOPL II chapter I wrote "The Early History of Smalltalk". — Alan Kay

reply
Say you have a Car, Engine and Dashboard object.

Let's not have dashboard access the temperature by doing `GetSurroundingCar().engine.temperature`

If the dashboard needs to get the temperature from a sensor in the engine, it should be able to "talk" to the sensor, without going through car object.

In ideal OOP, a "method call o.m(...)" is considered a message m being sent to o.

In practice, field access, value and "data objects" etc are useful. OOP purism isn't necessarily helping if taken to the extreme.

The pure OOP idea emphasizes that the structure of a program (how things are composed) should be based on interactions between "units of behavior".

reply
I had to learn OOP with common lisp (CLOS) and smalltalk to understand this. Now, I’m leaning towards C, because it’s easier to model a problem with struct and function and not have to deal with the flavor of OOP that some languages foster.
reply
1. Avoid passing live instances (by reference) to other instances as much as possible. Because you don't want many instance references to be scattered too widely throughout your codebase. This can cause 'spooky action at a distance' where the instance state is being modified by interactions occurring in one part of the code and it unexpectedly breaks a different module which also has a reference to that same instance in a different part of the codebase. The more broadly scattered the reference is throughout the codebase, the harder it is to figure out which part of the code is responsible for the unexpected state change. These bugs are often very difficult to track down because stack traces tend to be misleading because they don't point you to the event which led to the unexpected state change which later caused the bug.

2. Avoid overly complex function parameters and return values. Stick to passing simple primitives; strings, numbers, flat objects with as few fields as necessary (by value, if possible). Otherwise, it increases the coupling of your module with dependent logic and is often a sign of low-cohesion. The relationship between cohesion and coupling tends to be inversely proportional. If you spend a lot of time thinking about cohesion of your modules (I.e. give each module a distinct, well-defined, non-overlapping purpose), the loosely-coupled function interfaces will tend to come to you naturally.

The metaphor I sometimes use to explain this is:

If you want to catch a taxi to go from point A to point B, do you bring a steering wheel and a jerry-can of petrol with you to give to the taxi driver? No, you just give them a message; information about the pick up location and destination. This is an easy to understand example. The original scenario involves improper overlapping responsibilities between you and the taxi service which add friction. Usually it's not so simple, the problem is not so familiar, and you really need to think it through.

We understand intuitively why it's a bad idea in this case because we understand very well the goal of the customer, the power dynamics (convenience of the customer has priority over that of the taxi driver), time constraints (customer may be in a hurry), the compatibility constraints (steering wheel and fuel will not suit all cars). When we don't understand a problem so well, an optimal solution can be difficult to come up with and we usually miss the optimal solution by a long shot.

reply
nice post, lately ive been dealing with concurrency, between threads and processes. trying to keep it cross platform as well, its a lot to learn. if you have large buffers and want to keep some semblance of performance, its VERY interesting understanding all the transfer mechanisms and cache levels involved. i feel these are the sorts of things my education skipped, it was all very focused on the static structure of objects not the dynamics of data transfer.
reply