Hacker News new | past | comments | ask | show | jobs | submit login
The single assignment cargo cult (tonyarcieri.org)
12 points by nickb on July 27, 2008 | hide | past | favorite | 20 comments



"The overwhelming majority of programmers have only used languages with multiple assignment. Multiple assignment is the de facto standard, and single assignment is one of the things that makes Erlang weird to new programmers. "

So what? "But it feels weird!" is not an argument. It is an excuse.

It's like when people dismiss Lisp because of all those parentheses; you get over it, and eventually find more interesting things to focus on.


But, it seems like it should be relatively simple to make the compiler convert your multiple assignment code to static single assignment form at compile time, with relatively little trouble. If there is any good reason not to do this, or if it is impossible/difficult, then I would understand single assignment. I just think the reason for it needs to be explained.

Similarly, if single assignments aims to satisfy a different set of tradeoffs, and somehow grants some advantage that multiple assignment does not, it seems like these should be clear enumerated.

In Lisp, for example, when new programmers complain about the parens, we usually explain that the they allows code to be equivalent to data and make the code directly be a parse tree. Which, among other advantages, makes macros far easier and cleaner.

I haven't seen any similar advantage expressed other than some hand waving about concurrency. If single assignment makes concurrency easier, how does it do so?


The reasoning that appeals to me most about single-assignment is the idea that it makes it easier to reason about what actions could have altered the state upon which the currently considered code operates. It makes explicit which code branches a particular expression is dependent upon. Supposedly this simplifies refactoring and reasoning about code. This justification sidesteps the idea of facilitating concurrency. It's similiar to the reasoning behind pure functional approaches where side-effects are avoided.

I've personally worked with code where single-assignment would have avoided bugs which were introduced because later modifications did not completely account for previous possibly state-altering actions. On the other hand this article shows places where enforcing single-assignment on the programmer makes reasoning yet more complicated.

I'm not completely sure what my final opinion on the subject is. Side effects, state, and assignment can all be considered orthogonally and I'm not sure what the best way is to deal with them. Maybe Haskell has the answer with its Monads, but I haven't examined the language closely yet.

One thing I liked about Pascal is the difference between the := (assignment) and = (equality) operators. You also had Procedures with side effects and Functions without. Separating these concepts definitely added to the pedagogical utility of the language.

As I learn more about these concepts I find myself more explicitly managing assignment, state, and side-effects in my code. Even in imperative or OO workaday languages such as PHP or Java. I will attest that it does make a difference in the ease with which I can decouple and maintain an ordinary codebase.


I've also found that avoiding statement-order dependencies (like those introduced by multiple assignment) is good practice even in Python/JavaScript/Java/C. Why make it harder to reason about your program than it already is?

The example in the article could easily be solved if Erlang had a function composition operator, eg. Haskell's

   baz . bar . fab . foo $ x
or Eve's

   x -> foo -> fab -> bar -> baz


Now I'm no Erlang master, but why not do:

  X1 = baz(bar(foo(X))).
Or, if you want to be fancy:

  X1 = lists:foldl(fun (F, A) -> F(A) end, X, [fun foo/1, fun bar/1, fun baz/1]).
Obviously the fun thing about the second way is you could have code make up any arbitrary list of one argument functions to apply in succession. If you know what functions you are going to use ahead of time, then the first way is much clearer.


I have no idea. That's what I would've done.

The only thing I can think of is that the article's author doesn't want to have to move between the parentheses. His initial complaint was that adding a function 'fab' in between foo and bar means you have to rename everything. If you chain the functions in one long expression, you need to navigate to between bar and foo, type 'fab(', then add a separate parentheses onto the end.

It also gets really ugly when foo, bar, and baz are instead line-long expressions, which they often are:

    X1 = big_long_expression_containing_some_stuff_and_baz(
              big_long_expression_containing_some_stuffand_bar(
                     big_long_expression_containing_some_stuff_and_foo(X))).
While if you have a sane linebreaking policy (as both Haskell and Eve do), you can do:

   X = big_long_expression_containing_some_stuff_and_baz
     . big_long_expression_containing_some_stuff_and_bar
     . big_long_expression_containing_some_stuff_and_foo
     $ x


> It also gets really ugly when foo, bar, and baz are instead line-long expressions, which they often are

I sometimes use variables to "take a breather". You do some heavy, syntax and/or meaning laden work, and put the result of all of it in 'current_velocity' or whatever, and that conceptually helps you to move on to the next thing with the result of what you've just done firmly in mind. Single assignment doesn't prevent that, obviously, but sometimes functional code uses them less and just uses a bunch of nested functions. Those Haskell examples you posted seem like a nice idea, by the way.

http://journal.dedasys.com/articles/2007/01/17/clockwork-lan...


Although some people, when faced with a problem they don't understand, will just give up and do something else instead.

Admittedly, I don't have any metrics on how many people have abandoned Erlang because of single assignment, or Lisp because of the parentheses, but I'm sure such people exist.

And you might say that it's their loss, and I'd probably agree with you.


> And you might say that it's their loss, and I'd probably agree with you.

Enough people give up, though, and it's your loss as a user of that language. Say out of every 100 people that give up, 1 is someone who is capable of contributing something good back, and 10 are people who might have answered questions on mailing lists or IRC, or written some blog entries or something. Languages do have network effects.


Sure, but it's hard to say if adding or omitting a feature is going to be a net win for a growing user base. All things being equal, I'd rather a somewhat smaller user base for a language that is more easily internalized than a larger user base where you really do need the extra help to understand all the quirks of a language.

Were that language design so simple ...


> Sure, but it's hard to say if adding or omitting a feature is going to be a net win for a growing user base.

I meant more along the lines of libraries to do useful things, which is a win pretty much any way you look at it.


"And you might say that it's their loss, and I'd probably agree with you."

Indeed I would say that. But there's another side. When a language allows for slippage in conceptual integrity for the sake of familiarity, everyone loses.

For example, I'm a big fan of Ruby, but there a number of places where consistency in language axioms was dropped so as to supposedly make it more intuitive. It does so at the cost of everyone having to remember both the fundamental concepts (e.g., "the last expression evaluated in a method is the return value") and the exceptions ("except when the method name ends with '=').

Lazy people like me think that's just extra work. :)

I don't know Erlang, but I suspect that if some form of "multiple assignment that isn't really the multiple assignment you're used to" were allowed, people would still have problems with it, but they would be harder to understand and resolve based on core language principals.


Your last paragraph show you have the right hunch.

Erlang has a pretty simple binding compared to, say, ML. In the statement

X = Expr, Body

Evaluates Expr, binds it to X and evaluates the body with X bound. This is akin to variants of let-bindings found in so many other languages. The difference is, however, that the binding can not be shadowed. If body contains, say:

X = Expr2,

it is taken as an assertion: Evaluate Expr2 and compare it to X, crashing the program if Expr2 is not equal to (in effect) Expr. What some miss is that this is not mutation, but binding. In

X = 0, F = fun (Y) -> Y + X end, X = 1, F(4).

The X inside the body of F is bound to 0 and not 1 when F gets evaluated because any semantics apart from this would be silly in a functional language with lexical scope. And since Erlang has no concept of Let-bound scopes in functions it would probably give some of the problems you mention.

I guess the Prolog-history plays a part in the decision, but also the fact that explicit (single) assignment is a really good tool for weeding out bugs: Any use will explicitly refer to exactly one def. Several compilers that allow shadowing of let-scopes even go as far as including a warning when you shadow variables!


Interestingly scala makes the choice fairly explicit - val or var - your choice. In practice I find I default to using val - as I know when I debug I only have one place to look for where the value is, and there is generally less cursing when a value mysteriously changes (it doesn't).

(Java has final, and C# has 'const' which is kinda similar). Although in their case you have to choose extra verbosity, but its often worth it.


> If you’re writing code like in Damien’s example and you want to be able to insert lines without changing a bunch of variable names, I have a tip: increment by 10.

    10 PRINT "BASIC LIVES AGAIN!!"
    20 GOTO 10


So essentially, what they're saying is, Erlang doesn't let you program in C? So much for that language, eh? Psh.

-

I don't have any real Erlang experience, but in every functional language I've ever touched, doing that would involve function composition, not manually stuffing the value thus far into a box. (Wasn't Erlang at some point based on Prolog, btw?)


The dichotomy between single assignment and variable-name reuse seems to be false. In OCaml, you can just rebind the same variable name again halfway a function body, which will cause a new variable with that name to be bound, having a scope of the rest of the function body. Everybody wins.


Or, you can use explicitly use a reference variable or mutable variable: (# lines are input, - : lines are response)

  type egg_carton = { mutable eggs: int }
  let my_c = { eggs=12 }
  let breakfast carton = carton.eggs <- carton.eggs - 2

  # my_c;;
  - : egg_carton = {eggs = 12}
  # breakfast my_c;;
  - : unit = ()
  # my_c;;
  - : egg_carton = {eggs = 10}
and

  type food = Pancakes | Eggs
  let eat f = f := None
  let short_stack = ref (Some Pancakes)

  # short_stack;;
  - : food option ref = { contents = Some Pancakes }
  # eat short_stack;;
  - : unit = ()
  # short_stack;;
  - : food option ref = {contents = None}
I think the best part here is that OCaml doesn't use = to assign values, it uses a left arrow (<-) for modifying mutable variables and := for updating what value a reference is pointing to.


he beats us over the head with the fact that gcc uses ssa yet seems to think that this fact makes general mutability desirable at the language level. the compiler is not a magic wand. hey why not make everything a string too? works for tcl!!!!


Everything is representable as a string in Tcl, but internally that's not the case, nor has it been for over 10 years.

Tcl's a very interesting language by the way - more than most people give it credit for - and was very successful in its heyday.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: