For some reason I started thinking about Alan Kay’s definition of OOP, and I remembered that he emphasized the importance of messaging. Since Elixir has processes which communicate via messages I thought it would be fair to say that Elixir is at least partly comforming to the purest definition of object-oriented design.
Some rando called “Joe Armstrong” has said the same thing:
Joe Armstrong: Erlang has got all these things. It’s got isolation, it’s got polymorphism and it’s got pure messaging. From that point of view, we might say it’s the only object oriented language
Ralph Johnson: The thing about Erlang is that it’s in some sense 2 languages, at least you program it 2 levels because one is the functional language that you use to write a single process and then there is what you think about all these processes and how do they interact, one process is sending messages to the other. At a higher level, that Erlang is object oriented, at the lowest level it’s a pure functional language and that’s how it got advertised for a long time.
If we were to put abominations like inheritance aside from classical OOP languages, elixir/erlang abstractions (namely genservers) are not that different from a class in let’s say java. Message passing actually is a solution to a thing that was solved differently by languages like java, concurrency.
All processes(objects) in elixir/erlang are already a concurrent entity, in contrast to an object in java, that is a data structure (state machine). These are 2 different views regard to concurrency, one says that it is better to have small concurrent ready parts and the other one says that we can have chunks of code that we might want later to make concurrent.
It is easy to understand why languages like java were designed this way, for a very long time all the code would run on a single cpu or core and in terms of that, this approach is much more efficient in both short term and long term.
Now I can’t say for sure, however I think erlang never had in mind (at least at the beginning) the concurrency model to be scaled on multiple cores like we use it today, it was designed to have the fault tolerant nature and small concurrent pieces (processes) with their own memory and communication channel was the way to achieve it. Maybe @rvirding can shed some light on their decissions.
From a performance standpoint, I cringe at the thought of using concurrency to implement abstraction and encapsulation, but it is interesting to see how far you can take this idea. Tony Arcieri (of Celluloid and Revactor fame in Ruby-world) developed a language more than a decade ago called Reia which took this idea to its logical conclusion by implementing something with Rubyish object semantics on top of Erlang by using message passing. I also know that the earliest incarnations of Elixir had more rubyish semantics, don’t know if it was at all similar to this idea; that was before I got involved in 2013.
When you send a message between processes in Elixir, the data passed as arguments are copied into the heap of the receiving process. When you call a method on a Ruby (or Java, C# apart from unboxed primitive arguments) object, any arguments are passed by a reference, memory is not copied. This makes Erlang more resilient and scalable in the large and avoids complications like concurrent garbage collectors, but in the small there are overheads that do not exist in traditional systems. Also these are not the only overheads; when objects are bound to processes in this way, they can become a bottleneck, and they introduce more complicated failure modes (which Elixir/BEAM is well equipped to manage, but they still exist).
These are some of the reasons why we don’t advise people to use GenServer as a general abstraction method in Elixir, but to only use it when concurrency is actually one of the problems to be solved, rather than a mechanism for solving unrelated software design problems.
To be fair, while this is better for performance it’s also one of the things, which causes a lot of grief with OOP. You not only have a lot of stateful stuff, but also seemingly everybody has access to public state all the time.
I totally agree with you from the performance standpoint, however I want to introduce another point: software resilience in context of the development. Languages like c#, java, ruby offer those optimizations on behalf of development complexity.
While this is kind of a stupid argument from an engineering point of view, I could argue that nowadays the ability to write resilient software with minimal investments (developers and time) is as important if not more important than performance.
Essentially the same thing happens once you start passing pids around. Everyone has access to whatever that pid will give them. With good design, that can be managed in both paradigms. Look, there is a reason I use Elixir; I believe in its model. But I don’t think its capabilities map well to OOP semantics, and I don’t consider that a problem either as I don’t much care for those semantics.
True, but we have more than just pids to store data in. I’m not so much argueing about what’s possible, but what’s the default. And yes some OOP languages have some datatypes, which are not objects, but as soon as you get to array or hashmaps or whatever dictionary type of the language this gets rarer.
I don’t really know how comment on this. Just some points (hopefully not too confusing):
Concurrency and fault tolernace were fundamental properties of the systems in which we were interested. Without these any solution was not interesting, however cool it might be.
For us concurrency and parallelism are different things, related but not the same. Concurrency is all about the problem and how you structure your system to solve it, while parallelism is a property of the hardware on which you are running.
Our goal was to create language, and ways to use it, which supported us in building these types of systems.
This is why we called them processes as that was how we were thinking.
The functional nature actually came later. It started out as Prolog and then migrated to a functional language as it better fitted what we felt we needed.
We were never OO at least clasical OO! At all! There is an OO chapter in the original book but that was more written as a joke. Shh that is a secret.
We got the feeling that OO languages, at least in those days, were very sequential and not concurrent. While they might talk of “sending messages” it was really just doing a function call. Which was what we didn’t want.
Doing the right error handling worked much better with processes.
I could go on here but think operating systems!
EDIT: We also felt that OO was very mutable, shared and global concerning data. Everything we didn’t want!