Ralph Johnson: Erlang, the next Java

A nice blog post about Erlang. Nothing new here for those following the Erlang story, but a nice summary of several important themes none the less. Some choice quotes:

The thing that bugs me about [Armstrong's] book (and about his talks) is that he make more fuss than he should about the functional language aspect of Erlang and not enough about the OO aspect. In fact, he denies that it is OO.

Processes in Erlang are objects. At the beginning of my course on OO design, I explain three views of OO programming. The Scandinavian view is that an OO system is one whose creators realize that programming is modeling. The mystical view is that an OO system is one that is built out of objects that communicate by sending messages to each other, and computation is the messages flying from object to object. The software engineering view is that an OO system is one that supports data abstraction, polymorphism by late-binding of function calls, and inheritance. Erlang is a perfect example of the actor model, which is an example of the mystical view.

Moreover, given several kinds of processes that have a common protocol and that share some things in common, it is easy to factor out the commonality into a function that they can both call. This is similar to class inheritance. So, you could even say that Erlang supports inheritance...

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Too eager to see object orientation?

Moreover, given several kinds of processes that have a common protocol and that share some things in common, it is easy to factor out the commonality into a function that they can both call. This is similar to class inheritance. So, you could even say that Erlang supports inheritance...

The first part about how Erlang processes are a kind of object which exchange messages is a good analogy, but this part seems contrived to me. As I understand it, factoring out common functionality into a function can be done in any language that supports the function abstraction. It is a stretch to call this inheritance, even though it is related to implementation inheritance in OO languages.

And I do see Erlang as eminently functional, more so than object-oriented, for example. Maybe it is because we all see what we want to see...

we all see what we want to

we all see what we want to see...

Quite.

I agree. The author's

I agree. The author's attempt to see 'inheritance' in function abstraction ignores the 'is-a' quality of inheritance.

I wonder if what they have

I wonder if what they have in mind is the duck-typing aspects that result from the dynamic-typing aspect of the language, not simply functional abstraction.

Type Classes...

Well, it's hard to say -- the 'record' system in Erlang wouldn't be much used for architecture in the way that OO people use inheritance; but 'behaviour' would. The system is a little like Ruby mixins -- implement the callbacks and you get the interface functions for free -- but static, since it's a module attribute. Static mixins make me think of Haskell type classes...

I think behaviours should be

I think behaviours should be discussed in this context, but I am not sure me that this is what Ralph Johnson had in mind.

Of course, it really doesn't make too much sense to use "OOP" and "inheritance" as generic terms referring to any kind of abstraction mechanism, modularity, or polymorphism...

No Free Lunch

Quoting the article:

[Erlang programmers] are more likely to write [the application] as thousands of processes. It doesn't hurt performance on a single processor and makes it likely that it will make good use of a multiprocessor. Then, put it on a ten processor system and make your system run ten times faster (probably eight or nine times faster, but that is still pretty good).

If only it was that easy... Amdahl's Law tells us that an implementation's sequencing points limit the speedup behavior.

A number of interesting and basic problems are inherently sequential, so it's not that easy to find a scalable parallel decomposition of these. Perhaps one can find some decomposition, but then it might not scale all that well, especially when comparing against the best available sequential program that solves the same problem. We may find that we need considerable computing resources to outperform a well-tuned sequential program.

Suddenly, we find ourselves entangled in a twisty maze of low-level idiosyncracies. Context switch overhead, be it from the OS or some VM, begins to count. Pretty gravely even when comparing against a tight inner loop which takes advantage of optimizing compilers, for example. So does locking or message passing overhead.

Cache management becomes more involved: we don't want one process to kick another process' working set out of L2, just because both are scheduled on the same execution unit (EU), or the OS decides to migrate processes to different CPUs with each time slice. Should we bind processes to EUs or will that hinder things? On the plus side, more EUs means we might have more cache available to play with.

Suddenly, we might find that cores and "real" processors make a difference (program scales only with the number of processors, but not with the number of cores), and we remain puzzled until we look at the actual processor architecture. Things are even worse^Winteresting with Hyperthreading technology, which shares even more hardware between execution units.

Another angle: suddenly an application becomes non-deterministic and less predictable due to processes being scheduled differently. Debuggers become less useful, formal verification techniques may find interesting bugs, which are hard to reproduce.

In reality, scalable parallel applications need careful design, and more often than not intimate knowledge about the targeted hardware, perhaps even more than sequential programs. From my own experience, and that of students and colleagues, the right decisions require thinking about a problem in unfamiliar, and sometimes novel ways.

For example:

  • Fine-grained decomposition might exhibit more parallelism, but it might also incur more book-keeping overhead. Too coarse-grained, and processing time gets wasted. The "right" granularity is often not easy to find. Sometimes, it's input dependent.
  • A little work duplication can decouple tasks and lead to less sequencing points, e.g., obviating the need for a lock. This might give a huge boost in scalability. Too much duplication and the scalability goes down the drain.
  • In asynchronous systems, a producer might generate messages faster than consumers can handle them. Message queues can overflow or take up significant system resources. This can be worked around by throttling or blocking the sender, but it needs to be taken into account.

Lots of choices, but there is no free lunch to be had... Anyway, I am looking forward to more research efforts in that direction.

The hardware is ready. Are we?

Amdahl's law

is really profound.

Very insightul

Very insightul!
Truly, taking advantage of multi-proc/multi-core in an efficient manner is no small thing.
From my layman's perspective of implementing various internet oriented applications in a scalable environment - the simpler the application, the better it scales.

A fine example would be 'postifx' MTA. It has a process running for each significant bit in e-mail delivery lifecycle - incoming connection, outgoing connection, header rewrite, actualy delivery to mailbox and so on. The end result is that, the application code is simple and OS takes all the trouble to schedule processes.
It seems that the granularity is on the 'just-right' side of things.

I think that a lot of programmers don't give credit to OS to efficiently schedule tasks, when actually from a bird's eye view which only the OS kernel has, it is possible to correctly predict when to execute this or that

Some of the problem are common

To be fair, some problem such as: [[Cache management becomes more involved]] are true for many parallel programming model, not just Erlang..

I tend to summarize your points as: it's usually easy to decompose an application into several tasks, but it's hard to ensure that this decomposition use efficiently the hardware.

Also (if memory serves!), this is also true for Erlang which use a VM which is not that efficient: sure you get a lot of scalability but the mono-CPU performance figures are quite lower than if the application was programmed in C.

perspective

Once again, perspective matters.

Chapter 17 Object-Oriented Programming
17.1 Basic Concepts
17.2 Mapping to Erlang
17.3 An Object-Oriented Interface
17.4 Object-Oriented Programming
17.5 Object-Oriented Design
"Concurrent Programming in ERLANG" 1996

Work *with* a language ..

Often, when people come to a new language/environment with some baggage from developing using some other language, they try to look at the new environment through the same lens that they found effective in the previous one. This strategy almost invariably leads to disaster ... not even a less optimal solution but really a disaster. For the OOP fan, everything is an object. For the FP fan, everything is done using functions.

There is nothing brilliant about a language having OOP or FP features. They are just specific lenses. I love to work in a language where I can be a lens grinder ... so I can make the lens for and build a microscope or a telescope depending on what I want to look at. This is why I admire Dan Friedman's attitude in The Role of the Study of Programming Languages in the Education of a Programmer.

It's all about message-passing

Ralph Johnson is spot on! Read about the Early History of Smalltalk and you'll catch the scent that Erlang is probably the most object-oriented language around, more so even than Smalltalk. Joe has made this point himself!

This fact doesn't make it any less silly to write Erlang programs the way people tell you to write Java. :-)

:-)

:-)

Erlang <> Objective-C ?

Its not that Erlang-style message passing doesn't exist in other mainstream languages. There is a "oneway" qualifier that you can use when specifying messages that are part of a "formal protocol" in Objective-C. (see Apple's doc on this.) That's nearly the same as Erlang's message passing and, though I haven't done it myself, implementing distributed objects and similar reliability features in Objective-C is probably not all that more difficult compared to Erlang.

The difference lies in the cost of a thread/process. That Erlang has a functional core seems almost irrelevant to the message passing discussion, but it should not be forgotten that it is the very reason process creation can be made so cheap in Erlang.

That Erlang has a

That Erlang has a functional core seems almost irrelevant to the message passing discussion, but it should not be forgotten that it is the very reason process creation can be made so cheap in Erlang.

The surprising thing is that they don't take full advantage of it. Despite the fact that variables are immutable, the VM copies them when they are sent from one process to another, even on the same machine. I believe they do it that way to ensure that they can garbage collect each process' heap space with out regard for other processes.

"A Case for the Unified Heap

"A Case for the Unified Heap Approach to Erlang Memory Management" (http://citeseer.ist.psu.edu/452894.html) raises that question and provides some hard, empirical numbers on the cost of strict per-process heaps.

shared and hybrid heaps

Erlang does have some experimental support for zero-copy message passing, using the "shared" or "hybrid" heap models. However they are not default because of the GC impact - it's much easier to GC independent processes.

Is the copy merely time shifted?

I wonder if its merely a question of whether to copy an object at GC-time or do it during the message passing. It may be that a shared heap can give time-local performance benefits, but does it outweigh the cost of message copy in the long run? Also there is a question of reliability. Process-local memory is very robust and you can crash a process without affecting others (as we all know from OS history). Even Symbian OS has adopted it - each thread has its own heap.

The issue is moot as Erlang

The issue is moot as Erlang is purely functional, so no updates can occur to shared objects. This is why it can just swap in a shared heap GC without affecting application. Crashing a process is identical: the memory unique to that process simply gets reclaimed.

Robustness and Resource Accounting

PLT Scheme provides memory quotas in a shared heap. See Memory Accounting without Partitions. The security of the scheme has been discussed on the cap-talk list. (having a server spawn a subtask per client seems to solve their worries about non-determinisitic accounting). Kill-Safe Synchronization Abstractions describes how PLT Scheme ensures that killing processes won't cause deadlocks around shared data structures like message queues, and that's all you need beyond simple memory safety to make crashing processes safe in a pure message-passing language.

Kill vs. crash ...

"Kill" according to that paper doesn't seem to cover the case of a "crash". OS kernels may not always have the luxury of waiting for a process to leave some kernel object in a consistent state before terminating it. Java by nature doesn't permit access to illegal memory locations at the machine level, but a C program does allow that and an OS kernel must still live with it. It must take down the errant program from an interrupt handler, releasing all resources it acquired.

That sounds like a bit more than "kill-safe" to me. Is it?

See Memory Accounting

See Memory Accounting without Partitions. The security of the scheme has been discussed on the cap-talk list. (having a server spawn a subtask per client seems to solve their worries about non-determinisitic accounting)

That was my post that kicked off the discussion. I'm not clear on your proposal to spawn a subtask per client. IIRC, there were two main problems under discussion:

1. random ownership assignments could result in a shared service S being charged for resources used by its one of clients M; this is a DoS vulnerability which M could exploit to crash S.

2. a malicious client M could release handles to a resource B is co-operatively using which B is then charged for, and which places B over its quota.

Are you suggesting that for each shared resource that could be revoked maliciously, a subtask is spawned? I think this would probably be quite unmanageable, and a more principled accounting strategy would be simpler for developers to reason about.

The ultimate problem is that imprecise resource accounting and control leads to exploitable vulnerabilities, and it's not at all clear how to recover from the above situations. This is exacerbated by the fact that they are not even repeatable (non-deterministic), so you can't even test for them. In principled secure operating systems, like EROS and Coyotos, both 1 and 2 are not present because M must explicitly allocate the resources (and be charged for them), where in PLT scheme, allocation and ownership are implicit and separate. In EROS, S must still protect itself from the resources being revoked by M, but this recovery is understood, and quite doable.

I think the PLT scheme technique might be salvageable, which is why I pursued that discussion. Jonathan Shapiro proposed an interesting resolution: charge *all* parties sharing a reference for the storage; now its clear how the developer can reason about the resources his program will use, and the above situations are repeatable and testable, which is a marked improvement.

My Favorite Part

Where does the special efficiency of object-oriented design come from? This is a good question given that it can be viewed as a slightly different way to apply procedures to data-structures. Part of the effect comes from a much clearer way to represent a complex system. Here, the constraints are as useful as the generalities. Four techniques used together--persistent state, polymorphism, instantiation, and methods-as-goals for the object--account for much of the power. None of these require an "object-oriented language" to be employed--ALGOL 68 can almost be turned to this style--and OOPL merely focuses the designer's mind in a particular fruitful direction. However, doing encapsulation right is a commitment not just to abstraction of state, but to eliminate state oriented metaphors from programming.

                                                                                — Alan Kay, "The Early History of Smalltalk"

You should hear the howls I get from colleagues when I point out that last sentence...

Having spent a lot of time with Oaklisp and other, more traditional, dialects of Lisp, if there's one thing I get, it's that first-class lexical closures and objects can macro-represent each other, so the discussion about which is "better," in and of itself, isn't any more interesting than the discussion as to whether Pascal or C was better. What's interesting is the extent to which people who write software take the care implied in Dr. Kay's comment, and the extent to which a language does or does not require them to take that care.

Deep

Actually it's a very deep observation, and it requires a lot of experience to really grasp what it means.

Suggested exercise: Does the sentence still hold true if "information hiding" is substituted for "encapsulation"?

I think so

I think the bottom line is that exposing mutation is a form of tight coupling inasmuch as it might not be possible to extend the system without changing the exposed state transitions in a non-backward-compatible way. Encapsulation, whether through the mechanism of "classes," "modules," "records," "closures," or what have you, is one approach to limiting the exposure of state transition dependencies; another such mechanism might be "monads," for example.

The point that I try to emphasize in discussions like this is that when you dig a little into the foundational literature, you learn that the functional language fans aren't the only ones who think that focusing on state in programming is a bad idea that makes it difficult to create robust systems, and that object-orientation is yet another approach to removing state from the domain of discourse. The push-back to this point of view (IMHO) helps to demonstrate the corrosive effect of state-orientation on otherwise perfectly functional (no pun intended) minds.

Erlang shares state.


Joe makes too much of functional programming because he says that lack of mutable state implies no locks. However, it is really lack of SHARED state that implies no locks.

No locks are implied due to representing state as an Erlang process. State access is sequenced, and each message is handled atomically(can break with public ets tables ...).

It seems quite reasonable to

It seems quite reasonable to view Erlang this way. Consider that SCOOP for Eiffel is essentially also an Actor Model approach to concurrency, and Eiffel is very much an OO language.