Actors have become quite the popular topic. Besides Erlang, there's a famous library implementation in Scala and at least 3 for Java. But the "no shared state" propaganda is setting people up for failure. In last week's exciting episode I defined both what state is and what it isn't. It is the fact that something responds differently to the same inputs over time. It is not the details of how that is represented. Based on that I can now show why saying that Erlang style actors don't share state is totally wrong headed - that, in fact, if you don't want to share state the actor model is a very clunky way of doing things.
Before I go on, let me emphasize that I like Erlang. I like actors, too. It's a very nifty model. But it's not a stateless model. Let me show what I'm ranting against:
- "The actor model consists of a few key principles: No shared state"
- "Erlang is an inherently stateless system."
- "Erlang does not have [...] 'mutable state'"
The problem is that Erlang style actors can have shared, mutable (changeable), state. The model is all about shared state. If you didn't need to share mutable state then there are better alternatives than actors. And, importantly, even when you are using actors well you must think about all of the problems of shared mutable state. All of them. People who believe the propaganda are in for rude shocks.
The Situation
Last time I described a simple sock tube dispensing machine. Insert two coins, press the button, get a tube of socks. Insert too many coins and you get the extra coins returned. Push the button before you've inserted enough coins and you get nothing. Here's the diagram.
Imagine that Alice and Bob work at Crap Corp. ("Making High Quality Crap Since 1913"). Once a day they each like to saunter down to the break room and purchase a nice warm tube of socks. But, this is Crap Corp. and the machines don't take regular coins but Crap Corp. Coins instead. Each CCC weighs 40 pounds (very roughly 18.1436948 kilograms).
Naturally, Alice and Bob don't want to carry 80 pounds of crappy tokens around with them so they each laboriously drag a token down to a machine, insert it, walk back to their cubicle, grab another and repeat. Now, if Alice and Bob take their sock breaks at very different times that's probably going to work fine. But if they tend to overlap bad things happen. It's possible for Alice to insert her first coin, Bob to insert his first coin, Alice to insert her second coin and get an coin back (BONUS! she cries happily, experiencing the greatest joy she's ever experienced at Crap Corp.) So Alice pushes the button, gets her tube of socks, and merrily skips back to her cube. Well, maybe not skip exactly, but whatever you do when you're ecstatically happy while carrying 40 pounds of crap.
Then Bob shows up, inserts his second coin, eagerly smashes the button to get his well deserved tube of socks and ... gets nothing. Feeling cheated he pounds on the machine, kicks it, shakes it, and rocks it back and forth. Inevitably, the machine tips over, falls on Bob, and crushes him with all the tons of Crap Corp. Coins that have been inserted over the weeks. A tragic ending for somebody who just wanted some socks.
Now, that outcome isn't guaranteed even if Bob and Alice start about the same time. On the way to inserting her first coin Alice could be waylaid by the boss looking for his TPS report. As Alice patiently explains that a) TPS reports were never her job and b) they were discontinued three years ago and c) her eyes are on her face not her chest, Bob could have merrily taken the two coin trips and safely received a tube of socks without ever knowing the mortal injury he narrowly avoided.
Finally, Some Damn Code
Any time something unwanted can happen as the result of unpredictable delays, scheduler priorities, workload, etc you have a race condition. What could be more unwanted than being crushed by a vending machine? And what could be more unpredictable than a pointy haired boss? We can write this up exactly in Erlang.
In a file called sockmachine.erl. First, a little standard module definition and export business.
-module(sockmachine). -export([start/0, insertcoin/1, pushbutton/1, test/2]).
Here are the guts of the machine. zerocoins(), onecoin(), and twocoins() are the states of the machine. When one is called it blocks, waiting for an message in its inbox. Based on the message it gets it responds with {nothing} if nothing happens, {coin} if it needs to return a coin, or {tubeofsocks} for the win. It also then calls the appropriate function for the next state - which might be the same state. These are all private functions not exported by the module. Note, there are more clever ways to write this - but for explanatory purposes I like this.
zerocoins() -> receive {coin, From} -> From ! {nothing}, onecoin(); {button, From} -> From ! {nothing}, zerocoins() end. onecoin() -> receive {coin, From} -> From ! {nothing}, twocoins(); {button, From} -> From ! {nothing}, onecoin() end. twocoins() -> receive {coin, From} -> From ! {coin}, twocoins(); {button, From} -> From ! {tubeofsocks}, zerocoins() end.
Start spawns a new sock machine actor in the zerocoins state
start() -> spawn(fun() -> zerocoins() end).
insertcoin and pushbutton are rpc style convenience functions that insert a coin or push the button. Or did I get that backwards? Well, whichever, they each return whatever they recieve as a message back from the machine.
insertcoin(Machine) -> Machine ! {coin, self()}, receive X -> X end. pushbutton(Machine) -> Machine ! {button, self()}, receive X -> X end.
Test spawns as many concurrent test loops as requested to simultaneously pound one machine.
test(Machine, Processes) -> if Processes > 0 -> spawn(fun() -> testloop(Machine, 100) end), test(Machine, Processes - 1); true -> io:format("all test processes launched~n") end.
Testloop repeatedly walks through the cycle of inserting 2 coins and pushing the button. It calls helper functions that mirror the state of the sock machine to show what it expects to happen at each step, complaining when things don't go well.
testloop(Process, Machine, Count) -> if Count > 0 -> testzerocoins(Process, Machine,Count); true -> io:format("[~w] testing completed~n", [Process]) end. testzerocoins(Process, Machine, Count) -> case insertcoin(Machine) of {nothing} -> testonecoin(Process, Machine,Count); {coin} -> io:format("[~w] BONUS COIN!~n", [Process]), testtwocoins(Process, Machine,Count) end. testonecoin(Process, Machine, Count) -> case insertcoin(Machine) of {nothing} -> testtwocoins(Process, Machine,Count); {coin} -> io:format("[~w] BONUS COIN!~n", [Process]), testtwocoins(Process, Machine,Count) end. testtwocoins(Process, Machine, Count) -> case pushbutton(Machine) of {tubeofsocks} -> io:format("[~w] Got my socks.~n", [Process]); {nothing} -> io:format("[~w] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH~n", [Process]) end, testloop(Process, Machine, Count - 1).
Now fire up erl, compile, start a machine, and test it with only 1 running test loop
1> c(sockmachine). {ok,sockmachine} 2> Machine = sockmachine:start(). <0.38.0> 3> sockmachine:test(Machine,1). all test processes launched ok [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] Got my socks. [1] testing completed
Ah, sweet, sweet success! But now run another test with 2 concurrent test loops. 1 = Bob, 2 = Alice...or was that the other way around?.
4> sockmachine:test(Machine,2). all test processes launched [2] BONUS COIN! [1] BONUS COIN! ok [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] BONUS COIN! [1] BONUS COIN! [2] Got my socks. [1] Blasted machine ate my money! Give it to me! Rattle, rattle, ARRRGGHGHHRHRHGH [2] testing completed [1] testing completed
It's a litany of socks, bonus coins and crushed intestines. On my machine it's an oddly predictable litany, but in a real, distributed Erlang app it would be much more interesting and random litany as network delays would emulate pointy haired bosses even better than Erlang's scheduler.
Some Last Notes
With Erlang style programming, actors are the central unit of statefulness. Multiple actors can share access to one stateful actor. Hence shared state, race conditions, and ruptured spleens. Am I saying that Erlang or actors are bad? No, in fact I quite like them. What the Erlang model does very nicely is separate that which must be stateful because it is concurrent from that which is more pure computation. By making state so much more painful to write than "foo.x = foo.x + 1" the actor model encourages you to think about the consequences of sharing it. It also cleanly mirrors the mechanics of distributed computing and asynchronous IO. It's nice, but it's not stateless.
One last note. I started with "actors are all about shared state." Naturally one might ask "well, what about stateless actors - actors that don't change state or depend on state via IO?" Certainly those are viable uses of actors. But that's no longer concurrency, that's parallelism and IMHO futures, data flow variables, and Haskell's data parallelism are all cleaner ways to deal with parallelism. Someday soon I hope to write about them. In the meantime, the whole point of having the complexity of message passing instead of those simpler mechanisms is precisely to deal with the complexity of concurrent state.
One really last note. Sadly, simple straight up actors don't automatically compose very well. You can design a set of actors that interact correctly for one use case but that don't interact at all well when plugged into a different system. This is another aspect that actors share with traditional manual locks and shared mutable memory. To date the best known way to deal with composable state is transactions (optimistic, pessimistic, distributed, compensating, software, hardware, database, whatever). There are very nice transactional capabilities available for Erlang, but this is yet another area where the "no shared state" mantra can lead people to think that actors are the entire answer without needing anything else.
Try not to get crushed and good luck with your socks!
Postscript
It has been suggested in the comments that when people say that Erlang style actors don't share state they mean it doesn't share memory. First, I clearly defined state in the previous article as being different from its representation. But, just as importantly, I think that saying "we don't share memory" is a distinction without much relevance. It's mostly an implementor's point of view that doesn't reflect how a user must think about actors.
Here's some Erlang code for simple shared mutable variables. This isn't recommended Erlang usage, but it doesn't break the model in any way.
-module(variable). -export([malloc/0, fetch/1, store/2]). malloc() -> spawn(fun() -> loop(0) end). loop(Value) -> receive {fetch, From} -> From ! Value, loop(Value); {store, NewValue} -> loop(NewValue) end. fetch(Variable) -> Variable ! {fetch, self()}, receive X -> X end. store(Variable, Value) -> Variable ! {store, Value}.
And here's me using it.
1> c(variable). {ok,variable} 2> X = variable:malloc(). <0.38.0> 3> variable:store(X,42). {store,42} 4> variable:fetch(X). 42 5> variable:store(X, variable:fetch(X) + 1). {store,43} 6> variable:fetch(X). 43
And, since these variables are actors they are just as easy to share as my sock machine example.
Wow, I was really looking forward to reading more about socks, and you didn't disappoint me. I LOVE SOCKS! I hope when you write about that pairy-lil stuff you also include socks.
ReplyDeleteThe best part about socks is that they discourage sharing (especially when your feet are rank). That way you know they're *your* socks. Kinda like that her-lanj thing with its pretender's who don't want to share (greedy mimes).
It's funny, I was just thinking about this very topic. I would actually make the argument that you *cannot* do anything useful with actors which does not involve state, whether implicit or explicit. For example, consider the problem of writing a naive fib(n) function using actors for concurrency. It's easy enough to simply bang out something that sends asynchronous messages to two separate actors at each step, but getting the results back from those actors and collecting it in some usable form is somewhat trickier. One usually ends up doing something like this (Scala syntax):
ReplyDeletedef fib(n: Int) = {
val acc = actor {
react {
case Result(x) => react {
case Result(y) => loop { react {
case Query(_, tar) => tar ! Result(x + y)
}}
}
}
}
fibAct ! Query(n - 2, acc)
fibAct ! Query(n - 1, acc)
tar ! Query(n, self)
receive { case Result(x) => x }
}
Obviously this is a bit ugly, but the point is that this code is completely devoid of vars or any form of assignment. However, despite this, we are very much taking advantage of shared mutable state in the form of the `acc` actor. This actor has three possible states: no values, one value, both values. We change between these states in a fairly normal actor-like fashion (by queuing up a new react), but changing these states involves exactly that, *changing* the actor which is shared in multiple places by this point.
Now, this is completely deterministic and quite concurrent (assuming that I implemented the fibAct method correctly), but it is hardly stateless.
Daniel,
ReplyDeleteThat illustrates nicely a point I made in the previous article about how state can be hidden by making it local. Users of the fib function don't know that it is implemented stateful. It also illustrates one of my closing arguments - because state doesn't need to be shared, this code is easy to rewrite with futures and becomes cleaner as a result
def fib(n: Int) : Int = if (n <= 0) 1 else (future { fib(n - 1) })() + (future{ fib(n - 2)})()
"By making state so much more painful to write than "foo.x = foo.x + 1" the actor model encourages you to think about the consequences of sharing it."
ReplyDeleteI don't buy this argument. If a language makes something painful, it doesn't really discourage bad programmers from screwing up, it discourages good programmers from using your language because in the situations where you really need the "dangerous" feature, it is hard to use and makes code ugly.
Hi James,
ReplyDeletevery nice pair of articles.
Just my 2 euro cents: I still think the actors model is a "share nothing" model, meaning that one actor state cannot be concurrently modified, nor accessed by other actors.
So, IMHO, I think the "no shared state" statement still applies.
Cheers,
Sergio B.
I think that what you are talking about as "sharing state" is not what is normally meant when discussing actors/erlang processes not sharing state. You are talking about the application as a whole which has state and this state is shared by the actors/processes in it. What is normally is whether the actors/processes actually share data, whether multiple actors use the same memory. This is something completely different and at a different level.
ReplyDeleteSo your application has data which is shared by many processes but the processes themselves do no share dat.
@Sergio,
ReplyDeleteIn my example code two actors (Alice and Bob...or well, [1] and [2]) are concurrently modifying the state of the sock tube machine actor. The final outcome depends on a race between them.
@Robert,
Why is it important that actors don't share physical memory when talking about whether they share state? As I explained in the last article, state is what makes something react differently to the same inputs. Actors can clearly be stateful and they can be shared. And, as I showed in this article, this isn't just a theoretical distinction. It has real practical impact in the form of possible race conditions.
Every usefull application has state, so every usefull multithreaded/distributed application has shared state.
ReplyDeleteActors are usefull not becouse they excludes shared state, but becouse they are good analogy - it is intuitive for people, that when you are talking to two people at once, you have to remember who said what.
Your example with socks also is intuitive - I would be hard pressed to left my money in machine and go out :)
So - actors are nice analogy.
James, I'm not sure I grok your point. I tend to view Erlang processes more like state machines. They have internal state that can change based on inputs (messages) and they can send message to other processes when that happens. There is no way for a process to explicitly go in and change the state of another process without sending a message and without the receiving process accepting the message and acting on it. (well, you can, if you use publicly writable ETS tables, but that's cheating :). That doesn't mean you cannot have concurrency issues like race conditions or deadlocks. Your example is a good one and I could easily cook up some code that deadlocks. However that's just a symptom of bad application design and does not mean the processes share state.
ReplyDeleteJust my $0.02
PS: you can send simple atoms as messages, there's no point in encapsulating them in tuples :)
This comment has been removed by the author.
ReplyDelete@Mihai,
ReplyDeleteSo you're saying that if I write Java where my mutable variables are always hidden behind methods I've avoided shared state? Well then, Joe Armstrong will love Scala!
A more apt analogy would be Java classes where all methods are synchronized and arguments and results are passed as values, not references. Still not a complete analogy, but closer to the "no shared state" paradigm.
ReplyDeleteI can't speak for Joe Armstrong, but I'd venture to say that his appreciation of Erlang stems from more than just the "no shared state" idea. The actor model is just one part of what makes Erlang powerful.
I agree with the issues you raise in your post however I don't think this is about shared state but rather reentrance/transactions. Perhaps I am out of touch with the Erlang community, but when people talk about shared state I do not think they are saying each message operations are atomic/independent of any others going to that process, but rather all modifications to the state of a process require explicit operations on that process and any returned data is independent of that process.
ReplyDeleteI don't really get your point. As in your code, you can use a process to control access to a shared resource. But this is going no further than encapsulating state : you also have to throw a process at each concurrent activity to gain from the actor model. Alice and Bob are carrying independent transactions in your example. So the vending machine would spawn a transaction for each user. It would accept and refund coins, and relay the two coins together (the request becomes atomic) to the master process that would in turn dispense the item.
ReplyDeleteEverybody,
ReplyDeleteThe important part about Erlang's "no shared memory" model is that it's easy to distribute. Please stop assigning other attributes to it - those are trivial to implement in other ways. Please stop insisting that when you say "shared state" you mean something different from "state that has been shared." I understand what you're saying, I just think it's a page out of Orwell. It's deliberately self contradictory for no good reason. It's pure marketing speak without any attempt at rigor. Why is "state" in "state machine" the same as in "stateful object" and "stateful protocol" but different from "shared state"? If my examples aren't sharing state, what are they sharing?
I am pretty sure the disconnect here is at the definition of state. What exactly is this "state" you are talking about? Is it the states of the vending machine and its two users or the state of the entire system, which is the sum of the above?
ReplyDeleteEase of distribution is an important part of the "no shared state" model, but it is by no means the only advantage. It drastically cuts down an entire class of bugs with the corresponding increase in productivity.
@Mihai,
ReplyDeleteMy previous article explained what I mean by state. This one summarizes briefly at the top. State is that which makes something respond differently to the same inputs over time. The vending machine process clearly has state because sending the same message to it causes different results. That state is shared across [1] Bob and [2] Alice because you cannot reason about their individual behavior (in the code represented as printed messages) without understanding how both are interacting with the shared vending machine. That reasoning is much further complicated by scheduling indeterminacy which leads to race conditions.
just to counter-act all the orwellian weirdness going on, i thought your post was quite sensible. :-) and i greatly look forward to learning how you feel other approaches to concurrency and/or distribution fare!
ReplyDeleteI've got mixed feelings about this, I would rather call this something like "global state" rather than "shared state". The correctness of the system depends on the global state, which is the combination of the individual local states. To me sharing implies something more.
ReplyDeleteIf you were reasoning about these systems in a process calculus, global/local state would probably be more natural terms than shared/isolated? state.
You do have a point that the term "shared state" is being thrown around pretty loosely, maybe a comparison to a more parallel model would make it more clear what you mean.
A common 'counter example' is that by the definition you are using, two web browsers share state. After all, I can comment on this blog and that affects the anyone else looking at the page. Would you consider those web browsers sharing state or do you think the example is offbase?
ReplyDeleteYou might be interested in this discussion of what state is => http://forum.complexevents.com/viewtopic.php?f=13&t=115
ReplyDeleteHere's how the vending machine state space would be represented according to the view of the link above:
PushButton.x = x
InsertCoin.PushButton.x = InsertCoin.x
InsertCoin.InsertCoin.PushButton.x = x
InsertCoin.InsertCoin.InsertCoin.x = InsertCoin.InsertCoin.x
And here's an example of how a possible sequence of inputs would be reduced to its canonical form (i.e. one of the states) by using the representation above:
PushButton.InsertCoin.PushButton.InsertCoin.InsertCoin.InsertCoin.PushButton.PushButton
InsertCoin.PushButton.InsertCoin.InsertCoin.InsertCoin.PushButton.PushButton
InsertCoin.InsertCoin.InsertCoin.PushButton.PushButton
InsertCoin.InsertCoin.PushButton.PushButton
PushButton
lambda
As you can see there's no explicit representation of the state in the form of variables, therefore all possible incorrect behaviors arising from the concurrent access of such a representation by different threads are avoided. This is what is usally meant when saying that "state is not shared". A completely different thing is different users interleaving their requests against the same stateful entity which internally uses only one thread to serve those requests, in this case, obviously the stateful entity is shared by many users but the implementation of the stateful entity cannot introduce any incorrect unexpected behavior due to concurrency, in your example the vending machine is working as designed, no incorrect behaviors are occurring. You might not be happy with the design of the vending machine because it allows to happen what you explained but this is not happening because an incorrect implementation but because of an incorrect design of the interface (which should be providing a way for users to identify themselves and adapt its state space to avoid interleaving requests of different users)
Regards,
PatternStorm
PatternStorm,
ReplyDeleteThe race isn't that the vending machine somehow ends in an invalid state. The race is that the pair Bob/Vending Machine can somtimes arrive at an inconsistent mutual state - e.g. Bob expects socks the vending machine expects a coin.
It's perhaps easier to see if we add a third party, you. You are Bob's boss. One day, as a bonus, you give Bob two coins and says "you've done great! Go buy some socks on me!" Your desired result is for Bob to come back and say "oh, thank you, I'm willing to slave away for another 80 hour week now thanks to these nice warm socks." But it turns out that Bob might come back with joy and socks or he might get squashed by a toppling machine. The outcome depends on the race with Alice.
At no point does the machine "malfunction" in this scenario - it works as designed. But the outcome is unpredictable and often undesirable so some aspect of the "system" needs to be adjusted. The system here includes the machine, Bob, and Alice and any of them can be "fixed" to solve the problem.
James' point is a good one and seems obvious in retrospect, but it is often overlooked.
ReplyDeleteErlang does the heavy lifting to make process state changes deterministic. As James points out; Erlang does not automatically bestow safe handling of concurrent customers to a vending machine designed to work with a queue of customers politely interacting one at a time.
In mechanistic terms Erlang buys you nothing but deterministic memory access since it is simply another way of structuring concurrency. It is the human factors effect on the design process that really matters to me.
I find that Erlang encourages me to separate the work of a system into cooperating processes and lets me to quicky develop comprehensible systems. Like human team members, Erlang processes work best when they are given complementary tasks and clear direction on who to work with and when.
While James was typing his article, somebody else must have been typing on his keyboard, which resulted in this shared state article about vending machine :)
ReplyDeleteNo offense, James, but your example is not persuasive. Anything can be inferred from FALSE (buggy system), including the proof of existence/non-existence of shared state.
This argument is completely fallacious and simply an illustration that a system will misbehave if used inappropriately.
ReplyDeleteThe core point here is that your sock dispensing system is designed to handle one, and only one "session" at a time, yet you proceed to demonstrate what happens when multiple sessions are established.
So you confuse the state of the sock vending machine with the state of Alice and Bob. The machine only sees coins being inserted and has no context of if a particular coin was inserted by Alice or by Bob.
You are saying "Erlang style actors are all about shared state", but you are including state that is external to the Erlang server system itself. You could take it one step further and say "what if Bob's car breaks down on the way to work and he doesn't put a coin in the machine".
Or another example would be saying "an ATM machine doesn't work if two people try to use it at the same time".
Frankly, your argument is nothing more than a reiteration of the classic Garbage In Garbage Out.
Also, the example you give at the end that shows "shared mutable state variables" in fact does no such thing. All you have done is implement a server that changes state based upon invocations of your "store" function. This is disingenious to say the least, as I am sure you are fully aware that Erlang does not support mutable variables at all.
I'm surprised at how some of your readers have so badly missed the point or misinterpreted your code. It's weird.
ReplyDeleteMaybe if we translate it to a more familiar form, they will "get it."
import java.io.*;
public class SockMachine {
private int coins = 0;
public void insertCoin() {
if (coins > 1) {
returnCoin();
} else {
++coins;
}
}
public void pushButton() {
if (coins > 1) {
coins = 0;
giveSocks();
}
}
public void returnCoin() {
System.out.println("I won't take any more crap from you!");
}
public void giveSocks() {
System.out.println("Sock'd and amazed--you're cup overfloweth!");
}
public static void main(String args[]) {
SockMachine sm = new SockMachine();
sm.insertCoin();
sm.insertCoin();
sm.insertCoin();
sm.pushButton();
}
This is exactly what James' code is doing; his code just hides the state (which I called "coins" in the Java above).
If some Bob or Alice came along and used the SockMachine above, then everything would be fine, in serial. But if they did so in parallel, we have problems. And those problems would look surprisingly like the output from James' code!
Come on, people...it's a no-brainer. The Erlang code is absolutely sharing state (viz., the state of the sock machine).
This comment has been removed by the author.
ReplyDeleteGreat post! I'm just getting into Erlang now and have had some questions about stuff in the back of my mind.
ReplyDeleteI think the simple fact is that writing programs that have stateful parts means that there is absolutely no getting around the fact that the state will be "shared" at some point. Erlang is a different approach to sharing state, but ultimately, regardless of the language, unless you have a single-threaded program, there will always be the challenge of handling "shared state" correctly.
I'm not trying to say that all approaches work the same or equally well, I'm just trying to say there's no magic bullet.
Great article, exactly my view on the Orwellian shared propaganda concerning actors.
ReplyDeleteWrote about that too
http://codemonkeyism.com/actor-myths/
but not as good as you did. Funny one year later nothing has changed, the no-shared-state propaganda is in full speed .
Best
Stephan
Wrong. Your two actors are interacting with another actor, which is modifying its own state. This is a semantic argument, but it's important, because, as Sergio was saying, the processes are not SHARING their state.
ReplyDeleteYes, you are able to create a race condition, but you're doing so deliberately. In real-world applications you work to AVOID race conditions.
Just FYI, the point I'm making is that you're using actors wrong. You have deliberately circumvented the no-shared-state paradigm in order to make your example work. An actor like your vending machine should never be processing requests from multiple processes without tracking each request.
ReplyDeleteI can't even think of a real-world application that approximates your example. There are so many better ways to solve the type of problem you've described that it's overwhelming.
For example, consider the problem of writing a naive fib(n) function
ReplyDeleteusing actors for concurrency. It's easy enough to simply bang out
something that sends asynchronous messages to two separate actors at
each step, but getting the results back from those actors and collecting
it in some usable form is somewhat trickier. blending machine
sockmachine(coins) ->
ReplyDeletereceive
{coin, From} ->
From ! {nothing},
sockmachine(coins + 1);
{button, From} ->
From ! {nothing},
sockmachine(0)
end.
State exists on arguments.