Talk with Rich Hickey: Time is the New Memory
artima.com
artima.com
I've been following Rich's work for a while and I felt myself almost slip into confusion.
I guess he's trying to popularize the first principle of functional programming, but he's doing it in a confusing way that will inevitably breed further confusion. Look at the absolute silliness of the title for evidence of that.
I am not worried that it will confuse people. Ideas and perspectives that don't have merit will lose out to more powerful and clearer ideas over time. Also, I have always had more clarity only after passing through a state of confusion.
If there's one statement that the history of programming languages (to date) contradicts... :)
Actually, I think I'm reacting more to the slides of Hickey's recent talk about this, which he called "deconstruction" and kept comparing to Whitehead. I think Clojure deserves better than that. The signs of guru worship are already appearing. I would have said that's a bad thing. On the other hand, maybe it's the way to grow.
I agree with him completely that managing the timing of parallel processes is the most pressing complexity issue in computing today.
That mutable state has something to do with time is a tautology. Nothing in the recent discussion shows it to add anything significant to what we do or think about the problem. The interview title takes this doodling and amplifies it into some sort of grand theory. I'd be embarrassed.
I'm not criticizing Clojure at all, I'm criticizing this way of drawing attention to it (if that's what it is).
You are right, no one says "time management". I don't think they said "memory management" as often either, until there was automatic memory management (GC). But one thing is certain, most OO languages don't yet offer automatic _____ management, whatever it is to be called.
People do say they have a "race condition" or a "deadlock", or simply "data corruption" often without connecting it at all to the unmanaged mutation of their objects. And before there was GC there was just "heap corruption", "double deletes" and "memory leaks". Management implies a manager, and there isn't one yet, so why would anyone say it? What I am advocating is that languages provide automatic management of "time". It's not a novel idea, yet it is still missing, and I think it is missing because there is no place to put it in OO strategies which conflate identity with state, with weak or missing notions of object values.
But the phrase "mutable state" is broken, in a way that keeps people from understanding the problem and especially the potential solution. As soon as you consider state the value of something at a point in time, then mutation isn't the correct term, succession is, and the connection to time becomes clearer.
FYI, I neither wrote nor uttered the title. The interview was a recording of a casual, no prep conversation following my keynote. It was not meant to be profound, nor an advertisement.
Alas, mutable and immutable would appear to cover all the cases.
I think the modeling problem could be alternatively addressed by unlearning the notion (conceit?) that every program is a representation/simulation (which is where (domain) state creeps in) and instead embrace a descriptive/measurement approach. Or more precisely, to accept that a program is always orthogonal to the domain model and give up chasing after the holy grail/folly of seeking a paradigm to express "reality" in computer instructions.
One could argue that this is what FP is all about, but suggestion here is that the issue is fundamentally the conceptual view of the programmer and it can apply equally to OO or any other paradigm of programming.
For example, the "Date" case he outlines conceptually works if one is to suspend disbelief and accept the notion that a "Date" type object is really a point in time. "How can September 22, 2009 change?" But at some level this is just semantics. Would calling the "Date" object "DateDatum" cause a similar conceptual problem?
DateDatum lastUpdate = now (); //... doSomething(); lastUpdate = now();
Yes, this highlights the crux of the argument:
MutableDate lastUpdate = now(); doSomething(); lastUpdate.changeInPlace();
versus:
ImmutableDate lastUpdate = now(); doSomething(); lastUpdate = changedVersionOf(lastUpdate);
In the first case, when someone reads lastUpdate they get a chunk of memory that could change right out from under them, with no clear recipe as to how to obtain a consistent value. In the second case, they get a consistent value they can use without worrying or synchronization. The first conflates identity and state, the second separates them.
Now, all is not right with the second case, as there still aren't coordination semantics for putting new values in lastUpdate:
TransactionalRef<ImmutableDate> lastUpdate = now();
or
AtomicRef<ImmutableDate> lastUpdate = now();
or something similar would fix that. It's not a matter of removing mutable memory, but one of using it in a way with clear coordination semantics, other than "good luck with that".
Common Lisp programs make heavy use of side effects from imperative code, over destructive operations to the CLOS object system.
Even if you don't think Hickley is on the right track, how can you not like his combination of hands on implementation and search for theoretical soundness?
Not to discount Rich's work, but all of the ideas in Clojure have been well-known forever, and Haskell and ML implemented these ideas many, many years ago. Clojure lets you use these ideas with some Java libraries; that's its innovation.
Also, I don't really get the whole time analogy, but I can't help but think he wants Comonads, which Clojure does not have. (And due to its type system, will never be able to support well.)
As far as comonads, no - monads and comonads are essentially threaded through a single computation. The reference types I'm talking about are like, and for similar situations as, Haskell's MVars and STM TVars, which wouldn't exist if, e.g., state monads were sufficient.
A common use of MVars:
f x = pure computation
g h m = forever $ hGetChar h >>= putMvar m
do mvar <- newEmptyMVar
forkIO $ liftM f (takeMVar m)
forkIO $ g a mvar
forkIO $ g b mvar
<wait for exit condition>
All we do is read from two handles, concurrently, and eval f for every byte on either handle. The MVar is a natural way of expressing this, but it does lead to potentially undesirable side effects.Writing it in a functional way would probably be better:
f x = pure computation
g h f = forever $ liftM f (hGetChar h)
do forkIO $ g a f
forkIO $ g b f
...
There is now no shared state, since the problem doesn't actually require any. (We could use continuation passing style if we needed to keep state.)Anyway, I am still not sure how Clojure encourages the reference-less style.
I found the analogy to garbage collection just right. It's obvious now that for large parts of the problem space of writing computer programs, having garbage collection built into the language makes programs significantly easier to write and understand. Joel Spolsky made the argument that object oriented programming didn't do nearly as much to make programming more productive as automated memory management did, and I agree.
So why not take this other problem that is very hard to reason about, and build constructs into the language that take care of it for you automatically? I think the fact this seems so obvious to you that it's hardly worth saying points to how powerful the argument is. If it's so obvious, why is almost everyone still programming with mutable state and with so little support for "time management" constructs? Isn't this the same thing as stubbornly clinging to free and malloc in programs where garbage collection would work just fine?
My argument is not against closure or using immutable objects. My argument is against trying to rebrand a problem that has been well-known and well-studied for over 40 years.
Clojure does a bit of copying (memory management, not "time management") to force you to avoid concurrency problems that arise in some scenarios. Arguably that is a good thing, because people tend to think of concurrency when it's too late. However, it too ultimately has a mutable authoritative source where the same issues arise. Moreover, the hardest concurrency and consistency problems arise in distributed systems. Using Clojure certainly isn't the answer to that.
> Isn't this the same thing as stubbornly clinging to free and malloc in programs where garbage collection would work just fine?
People "stubbornly clinge" to programming with free and malloc (in C), because it allows them to have very specific control over how their program will behave. This allows you to perform very effective optimizations. Moreover, because it is cumbersome, C programmers will avoid dynamic memory, which will again make your program faster and more durable. The same thing will apply to mutable objects. Clojure simply restricts you to a certain model, which is not necessarily most efficient.
Does that mean Clojure is bad? No it means it's not the right tool for every job.
So does C. You don't use mutable array because they are the best solution. You use them because they're the best supported datastructure in C. Sometimes even when a shared immutable tree would be most efficient.
For a well-know, well-studied, and still unsolved problem, rebranding isn't wrong. It's the only way. I think pervasive mutation falls in this category. So does the widespread use of the Qwerty layout.
Also, you'd be amazed at how much control you have over the underlying system in Clojure; you just rarely have to wield it.
The same reason people still use C -- perceived efficiency, and 30+ years of history. Computers are just a big block of mutable state that runs instructions, after all, so the first thing that comes to mind is to program them without abstracting that away.
The reason we stick with this is that nobody wants to give up their favorite language (mostly for perceived employment reasons, if you don't know Java, you are unemployable or something), and "their favorite language" doesn't support immutability well. Try writing composible effectful computations in Java. Doesn't work. So you have to do something else. (If you have effectful computations that know about each other, you can put them in a class. If they don't know about each other, you basically have to manage the state manually.)
I experimented with this recently in Perl, via MooseX::Data (http://github.com/jrockway/MooseX-Data/). It ports various category-theoretic type classes to Perl. (You could do this in any language, although Java is not one of them unless you want to copypaste the standard liftM-in-terms-of->>= code into every class that implements "Monad", etc. Scala fixes this particular problem.)
Anyway, without special syntax, it ends up being a real mess. For example, consider incrementing a global state variable by one:
use MooseX::Data::Function::State;
sub S() { MooseX::Data::Function::State:: } # save typing
my $inc = S->get->bind(
S->new(
function => sub {
my ($state) = @_;
S->mreturn($state+1);
},
)->bind( S->put ));
And printing it: my $print = S->get->bind(
S->new(
function => sub {
my ($state) = @_;
say $state;
S->mreturn($state);
},
),
);
So now to inject 42 into the environment, increment it twice, and
print it: my $result = $inc->bind($inc)->bind($print)->runState(42);
This prints 44 and returns [44, 44], as expected. (44 is the state,
44 is the unlifted return value of $print->apply(44).)Anyway, this is an awful lot of code compared to the imperative version:
sub inc { $_++ }
sub prnt { print }
$_ = 42;
inc;
print;
return $_
(The functional version composes better, though; you can write a
function that acts on "the" shared state, and you can compose these
arbitrarily. In Perl, you only get one $_ per program.The tree-numbering example here is an excellent demonstration of composablity:
http://www.haskell.org/ghc/docs/latest/html/libraries/mtl/Co...
Note how there is no mutable state here, just a bunch of functions that pass (s,a) pairs to each other. And yet, you don't see that, you "see" composable mutable state. In this case, we are composing numberTree with itself recursively.)
To get back to my original point, with the right language, immutability is as expressive as mutable state. The increment example in Haskell:
import Control.Monad.State
type Step = StateT Int IO Int -- save typing
inc, prnt :: Step
inc = do
x <- get
let x' = x + 1
put x'
return x'
prnt = do
x <- get
liftIO.putStrLn $ show x
return x
main = runStateT (inc >> inc >> prnt) 42
-- or prnt << inc << inc
This prints: 44
(44,44)
It is worth pointing out that I combined the function calls with >>
instead of >>= so you can see that the values are being passed
implicitly (via get and put), rather than as arguments to the
individual functions, which would look like (putStrLn.show) . (1+) . (1+) $ 42
Anyway, it's a function of languages. If everyone was using Haskell,
very few programs would be using mutable state. But since they are
using Java, and the only state encapsulation tool Java supports well
are classes, that's what everyone uses. Very few people care about
"doing it right", they care about having a working program. And while
class-based state encapsulation is inferior to immutability in many
ways, most developers don't know this, and don't care.We may already know concurrency and consistency are hard, but we do little to identify and eliminate the root causes. Those who do can show the bright side, like Rich Hickey does, or they can finger-point the dark side, like I do: http://theroleplayingprogrammer.blogspot.com/2009/06/assignm...