> I don't use any of that stuff now and I write pretty much the same software I did back then.

I mean, if you're writing Erlang/Elixir code, then you kind of do have "classes, instances, and ancestors" — it's just that you and every other Erlang/Elixir programmer are just relying on an extremely ossified and low-tree-depth set of them for ~everything, in the form of the OTP-framework behavior modules.

What do I mean by that? Bit of a tangent, apologies in advance:

Elixir might have taken syntax inspiration from Ruby, but if you're comparing the Ruby runtime to the Erlang runtime, then the analogous OOP structure to a Ruby object isn't an Erlang record or Elixir struct — it's an Erlang process.

Originating from Smalltalk, OOP as an abstract model is all about things (capital-O "Objects") that interact by passing messages to one-another's opaque "receive a message" interfaces; where there can be no static guarantees about what a given Object will do with a message sent to it, because on the receiving end, a message will be fed into to code the Object controls to "parse" and "route" and (potentially) "respond to" the message; and the code being used to do this "parsing" and "routing" of a message, can be partly or wholly determined by the Object's internal state. (In the extreme case: you can send a closure to an Object, and it can replace its receive logic with the closure, effectively becoming a different Object entirely — even while still being the same lowercase-o object in reference-passing terms.)

In the Ruby runtime, all objects fit the OOP definition of an Object, and basically every operation is a message-send.

In the Erlang runtime, regular values aren't Objects and regular operations aren't message-sends; but Erlang-runtime processes fit the OOP definition of an Object, and `send`s to those processes fit the definition of OOP message-sends.

As such, Erlang/Elixir fundamentally has the capability to do `method_missing` shenanigans — it's what you'd get if you defined a custom (non-proc_lib) process whose toplevel loop runs through an explicit `receive` with an `AnyTerm ->` match-clause.

(In other words, you could totally build something like e.g. Ruby's DRb on top of local Erlang processes listening for arbitrary messages and proxying them to a remote. Not that the Erlang runtime needs this, given that you can just send messages to remote PIDs in a distribution set...)

But nobody would ever consider it idiomatic to implement these sorts of custom processes in Erlang. Why?

Well, the Erlang runtime generates several kinds of "system messages" that any given process is expected to handle in a "well-defined" way. If it doesn't, then your process won't be able to, say, participate in a supervision hierarchy correctly, or accept insertion of runtime trace hooks.

But there is no real way, given only Erlang syntax, to achieve anything like "compile-time receive-selector inheritance" — to have one `receive` statement "pull in" boilerplate clauses defined elsewhere. If you want to write a custom `receive` statement, then every bit of what it's matching on has to come from your own code. Your choices, when writing a custom `receive` in Erlang, would be to either copy all the boilerplate system-message-handling clauses into the code of each and every `receive` statement you write... or to not handle those system messages at all.

Given two bad choices, Erlang's initial programmers took the third path — the "any problem can be solved by adding a layer of indirection" path — and developed the OTP framework (specifically, proc_lib and gen_server.) You could say that OTP emulates "receive-selector inheritance" through composition: the OTP behaviors each have a static set of receive clauses, but recognize generic structured message types for "user messages", that they respond to by unwrapping and delegating those messages to what is, essentially, a receive function supplied by the user (`handle_call` et al.)

With OTP behaviors in hand, Erlang programmers then eschewed writing processes with explicit toplevel `receive` loops in almost all cases, replacing almost all such code with usages of OTP behaviors.

Later, when Elixir was being designed, it followed suit in exposing the actor system mostly through the OTP framework's abstractions.

I think this was partially because there was no actual "Elixir-idiomatic" exposure of the actor system at all for the first few versions of Elixir — you were instead expected to just call the Erlang OTP modules directly; and partially out of a desire to reuse code and seem idiomatic/familiar/inviting to Erlang programmers considering adopting Elixir.

But, here's the trip: unlike Erlang, where this inversion-of-control pattern was the only "good" solution, Elixir had other options here! It didn't have to do things this way!

Elixir doesn't have the syntax constraint that Erlang does: Elixir has hygenic macros. At any point during Elixir's development, someone could have easily:

• defined a `defreceive` macro, that gives a name to a `receive`-statement-as-reusable-AST that can be invoked elsewhere in the module;

• defined a version of this macro, that takes other, already-defined `defreceive`s (named `receive` statements) as "ancestors", and merges them into this `receive` statement's AST at compile-time.

With such a macro in hand, you wouldn't need to rely on the delegation approach of proc_lib/gen_server at all. Every process could have its own "bespoke" receive function tailored to its needs. To handle all the system messages correctly, there'd just need to be a receive-selector "root class" for your own defreceives to inherit from, that has all the same match-clauses in it that `proc_lib` does.

And from there, every stupid Ruby trick would also be applicable to Elixir processes. (Especially since the Elixir compiler / bytecode generator is inherently+inseparably part of the `elixir` library.) Singleton methods? Runtime self-modifying classes? foo.become(Bar)? Sure, those would all work.

In short: I think it's only an accident of history (of Elxiir copying Erlang's delegate-module approach to receive-selector "inheritance", instead of going for a macro-based approach) that held Elixir back from blossoming into an ecosystem of "stupid OOP tricks" that are just as messy and ridiculous as Ruby's.

(Well, that, and Elixir currently doesn't have any concept of typing for the message-ABI of a process — let alone type reflection ala Ruby's Object#class, Object#responds_to?, etc. Among Erlang-runtime languages, Gleam gets closest to having something like this... but its OOP abstractions (Subjects, Selectors, and per-actor Message types) are all about enforcing compile-time invariants on an actor's ABI type — not on reacting to an actor's claimed ABI type as it potentially changes over time.)