Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> 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.)



> I mean, if you're writing Erlang/Elixir code, then you kind of do have "classes, instances, and ancestors"

and

> 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.

Directly contradict each other unless you ignore what I wrote.


Let me restate my points succinctly:

1. OTP behaviors are OOP classes. And individual gen_* processes are instances of those classes. And those instances have proc_lib as an ancestor. So in a very literal sense, Erlang and Elixir have "classes, and instances, and ancestors."

2. But the few existing OTP behaviors are — idiomatically — literally all you get. Insofar as you're using the OTP framework, you don't get to define any of your own "classes." There's just the five that OTP comes with. And proc_lib is the only ancestor.

3. But that doesn't mean that the runtime-level potential for "first-class OOP" with all its quirks isn't there; especially in Elixir. Just because nobody has bothered to exploit it, doesn't mean that doing so wouldn't be possible. And, more oddly — just because the features go mostly unused, doesn't mean that it'd be possible to refactor the Erlang runtime to in any way remove those capabilities. They're inherent to the design of both Erlang and Elixir — even if it's only those six little "classes" that make use of them.

Which directly addresses your point:

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

You do still use "that stuff" (under other names, in a very formalized and circumscribed capacity.) A language that truly prevented you from doing any kind of OOP, no matter how under-the-covers, really wouldn't let you write the same sort of software!


You're certainly welcome to think these things, but you're going to have to take it up with Joe Armstrong, because he definitely didn't believe Erlang have "classes, instances" and while you're right that on a technical level you can share behavior from one module to another, it's absolutely not the same as Ruby's inheritance logic.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: