How do Erlang actors differ from OOP objects?

2019-03-25 15:13发布

问题:

Suppose I have an Erlang actor defined like this:

counter(Num) ->
  receive
    {From, increment} ->
      From ! {self(), new_value, Num + 1}
      counter(Num + 1);
  end.    

And similarly, I have a Ruby class defined like this:

class Counter
  def initialize(num)
    @num = num
  end

  def increment
    @num += 1
  end
end

The Erlang code is written in a functional style, using tail recursion to maintain state. However, what is the meaningful impact of this difference? To my naive eyes, the interfaces to these two things seem much the same: You send a message, the state gets updated, and you get back a representation of the new state.

Functional programming is so often described as being a totally different paradigm than OOP. But the Erlang actor seems to do exactly what objects are supposed to do: Maintain state, encapsulate, and provide a message-based interface.

In other words, when I am passing messages between Erlang actors, how is it different than when I'm passing messages between Ruby objects?

I suspect there are bigger consequences to the functional/OOP dichotomy than I'm seeing. Can anyone point them out?

Let's put aside the fact that the Erlang actor will be scheduled by the VM and thus may run concurrently with other code. I realize that this is a major difference between the Erlang and Ruby versions, but that's not what I'm getting at. Concurrency is possible in other languages, including Ruby. And while Erlang's concurrency may perform very differently (sometimes better), I'm not really asking about the performance differences.

Rather, I'm more interested in the functional-vs-OOP side of the question.

回答1:

In other words, when I am passing messages between Erlang actors, how is it different than when I'm passing messages between Ruby objects?

The difference is that in traditional languages like Ruby there is no message passing but method call that is executed in the same thread and this may lead to synchronization problems if you have multithreaded application. All threads have access to each other thread memory.

In Erlang all actors are independent and the only way to change state of another actor is to send message. No process have access to internal state of any other process.



回答2:

IMHO this is not the best example for FP vs OOP. Differences usually manifest in accessing/iterating and chaining methods/functions on objects. Also, probably, understanding what is "current state" works better in FP.

Here, you put two very different technologies against each other. One happen to be F, the other one OO.

The first difference I can spot right away is memory isolation. Messages are serialized in Erlang, so it is easier to avoid race conditions.

The second are memory management details. In Erlang message handling is divided underneath between Sender and Receiver. There are two sets of locks of process structure held by Erlang VM. Therefore, while Sender sends the message he acquires lock which is not blocking main process operations (accessed by MAIN lock). To sum up, it gives Erlang more soft real-time nature vs totally random behaviour on Ruby side.



回答3:

Looking from the outside, actors resemble objects. They encapsulate state and communicate with the rest of the world via messages to manipulate that state.

To see how FP works, you must look inside an actor and see how it mutates state. Your example where the state is an integer is too simple. I don't have the time to provide full example, but I'll sketch the code. Normally, an actor loop looks like following:

loop(State) ->
  Message = receive
  ...
  end,
  NewState = f(State, Message),
  loop(NewState).

The most important difference from OOP is that there are no variable mutations i.e. NewState is obtained from the State and may share most of the data with it, but the State variable always remains the same.

This is a nice property, since we never corrupt current state. Function f will usually perform a series of transformation to turn State into NewState. And only if/when it completely succeeds we replace the old state with the new one by calling loop(NewState). So the important benefit is consistency of our state.

The second benefit I found is cleaner code, but it takes some time getting used to it. Generally, since you cannot modify variable, you will have to divide your code in many very small functions. This is actually nice, because your code will be well factored.

Finally, since you cannot modify a variable, it is easier to reason about the code. With mutable objects you can never be sure whether some part of your object will be modified, and it gets progressively worse if using global variables. You should not encounter such problems when doing FP.

To try it out, you should try to manipulate some more complex data in a functional way by using pure erlang structures (not actors, ets, mnesia or proc dict). Alternatively, you might try it in ruby with this



回答4:

Erlang includes the message passing approach of Alan Kay's OOP (Smalltalk) and the functional programming from Lisp.

What you describe in your example is the message approach for OOP. The Erlang processes sending messages are a concept similar to Alan Kay's objects sending messages. By the way, you can retrieve this concept implemtented also in Scratch where parallel running objects send messages between them.

The functional programming is how you code the processes. For instance, variables in Erlang cannot be modified. Once they have been set, you can only read them. You have also a list data structure which works pretty much like Lisp lists and you have fun which are insprired by Lisp's lambda.

The message passing on one side, and the functional on the other side are quite two separate things in Erlang. When coding real life erlang applications, you spend 98% of your time doing functional programming and 2% thinking about messages passing, which is mainly used for scalability and concurrency. To say it another way, when you come to tackly complex programming problem, you will probably use the FP side of Erlang to implement the details of the algo, and use the message passing for scalability, reliability, etc...



回答5:

What do you think of this:

thing(0) ->
   exit(this_is_the_end);
thing(Val) when is_integer(Val) ->
    NewVal = receive
        {From,F,Arg} -> NV = F(Val,Arg),
                        From ! {self(), new_value, NV},
                        NV;
        _ -> Val div 2
    after 10000
        max(Val-1,0)
    end,
    thing(NewVal).

When you spawn the process, it will live by its own, decreasing its value until it reach the value 0 and send the message {'EXIT',this_is_the_end} to any process linked to it, unless you take care of executing something like:

ThingPid ! {self(),fun(X,_) -> X+1 end,[]}.
% which will increment the counter

or

ThingPid ! {self(),fun(X,X) -> 0; (X,_) -> X end,10}.
% which will do nothing, unless the internal value = 10 and in this case will go directly to 0 and exit

In this case you can see that the "object" lives its own live by itself in parallel with the rest of the application, that it can interact with the outside almost without any code, and that the outside can ask him to do things you didn't know when you wrote and compile the code.

This is a stupid code, but there are some principle that are used to implement application like mnesia transaction, the behaviors... IMHO the concept is really different, but you have to try to think different if you want to use it correctly. I am pretty sure that it is possible to write "OOPlike" code in Erlang, but it will be extremely difficult to avoid concurrency :o), and at the end no advantage. Have a look at OTP principle which gives some tracks about the application architecture in Erlang (supervision trees, pool of "1 single client servers", linked processes, monitored processes, and of course pattern matching single assignment, messages, node clusters ...).