Joel Spolsky: Can your programming language do this?
joelonsoftware.com
joelonsoftware.com
But... having actually spent some time in the trenches dealing with a hard problem on a massively parallel machine - more than once - I find it hard to believe that something like map/reduce or the like - or any given small-scale language feature is going to be particularly significant in terms of parallelizing any goddamn thing that's actually hard to do. I see a lot of fatuous claims that language feature X is the missing link in parallelism for the everyday working programmer but I don't see a lot of new solutions for anything hard as a proof of concept.
We've only had OpenMP and all sorts of other kludgy extensions for Fortran and C for what, about 15 years? I'm not saying that they're great or elegant or anything, but so many of the things that are hard about parallel programming are NOT FUCKING SOLVED BY MAP-REDUCE. Oops, sorry, shouty. But anything that can be solved by map-reduce wasn't enormously hard to begin with. Map-reduce was itself initially publicized in terms of 'less useful but easier for mere mortals than generalized parallel prefix' which made sense to me.
What doesn't make sense for me is all this frenzied enthusiasm for dragging parallel programming into the never-ending programmlng language abstraction wars; at least when the topics being discussed only touch on the very shallowest things needed by parallel programming. You want some respect, solve something hard.
Yes, you can do the same thing to each element of an array. Whaddya want, a cookie?
For example, say you are writing a double linked list. The push to head and push to tail functions may be completely the same, except one uses the head and one uses the tail, so you could just write one function and pass in the head or tail, right?
But you can't. The problem is that when you add something to one side, the links are the opposite. That is a new head goes to the right of the old head, while a new tail goes to the left of the old tail. In order to really do this, you also have to pass in a function.
Now, I've written a few doubly linked lists in Java (don't get mad, there _are_ good to do this), and in the end I just copy the function and make the inline changes. Trying to create the right interface and then pass in anonymous classes is almost impossible. And even if in my supreme cleverness I could figure it out, the code would be so confusing as to be unmaintainable.
Anyway, I would say that closures are immensely useful, not _just_ because of map reduce, but because of a whole class of examples that work in this manner.
http://www.youtube.com/watch?v=NWSZ4c9yqW8
As for techniques that can be used now: Haskell's `par` and `pseq` combinators allow adding parallelization to "hard" code after-the-fact pretty easily.
Granted, I was going after the low-hanging-fruit cases, but even so, I was impressed.
Making automatic implicit parallelism in Haskell is trivial - but it will yield too much parallelism and the overhead will trump the benefits.
The `par` and `pseq` combinators allow the programmer to specify which computations to parallelize explicitly to avoid parallelizing computations that are too small to be worth it and to allow the programmer to care for data-locality.
Despite being explicit, they are still far easier than other explicit parallelism mechanisms such as explicit threads because they are:
* So easy to throw in the program
* Guaranteed not to alter the semantics of your program - so you can just throw them in there and profile the performance changes -- and you know they won't break your program. They can alter the performance for better or worse.
Depends on your definition of hard. Algorithmically, yes the tasks are relatively simple, but try programming an 80,000 node MPI/PVM cluster to run general jobs and you'll get a very different definition of "hard".
But I do think that a practical solution will have to rethink not only programming languages, but the concept of programs. It will be made by a young and naive tinkerer, trying to solve a specific problem on actual parallel hardware, not by guys who have been entrenched in the field for decades. So it won't happen til that concrete technology (eg. 100 cores) is widely available. While there may be similarities with functional and object oriented programming (Alan Kay's version), it won't be based on them.
You're working a level above- map/reduce is easy there and things that would be completely unthinkable to maintain in assembly/FORTRAN/C are the real hard problems.
Just because easy things are easier in a language with map/reduce and gc and whatever else, doesn't necessarily mean that things that were hard before will fit cleanly into the mechanism.
Your idea that things that are 'completely unthinkable to maintain in Fortran or C' are somehow much easier in this kind of environment is just silly; there are multiple orders of magnitude more parallel code written in both of those languages than map/reduce and many of the classic parallel algorithms and (wince) design patterns like parallel prefix were written in these languages originally or are easily expressed that way.
The _hard_ algorithms are ones with sophisticated communication requirements that aren't well captured by map/reduce, so in this case, having a abstraction to deal with the easy cases has accomplished nothing towards making hard cases easier.
I'm not against abstraction, of course - just this premature triumphalism that picking a 'easy things become easy' approach gets you, especially if you don't solve the hard things.
http://en.wikipedia.org/wiki/OpenMP
I think it's just that a more complicated example is needed to really show the advantage of the first-class functions and such.
I don't believe it either, but it wasn't the point of the article. It was not about magically solving hard problems, but getting about parallelism for free where it is easy.
OTOH it is not apparent that those language features are actually indispensable for that. In most cases where 'map' would apply, it is quite possible for a compiler to detect that the loop body does not rely on its own previous executions and can thus be parallelized automatically.
But Map Reduce does, IMO, do one thing that MPI doesn't: the easy things are easy. Hard stuff is still hard, but easy things are easy. If you have a low communication algorithm that requires only local knowledge and fits into the MR paradigm well, hadoop + hdfs makes it really simple to write and reliably execute, including automatically handling things like (i) worker nodes dying, (ii) sorters dying, (iii) storage nodes dropping out, (iv) whole steps disappearing and having to be rerun.
FORTRAN had user-defined functions since FORTRAN II in 1958; see http://archive.computerhistory.org/resources/text/Fortran/10... on page numbers 5, 14, and 15.
Joel unfortunately completely misses the point of why C and Java suck at this stuff: you can use functions as values in them (anonymous inner classes in Java) but they aren't closures. And his comment about automatically parallelizing "map" is a little off-base; if you take some random piece of code and stick it into a transparently parallel "map", you're very likely to discover that it isn't safe to run multiple copies of it concurrently, which is why languages like Erlang have a different name for the "parallel map" function. The "map" in MapReduce is inspired by the function of that name in Lisp and other functional languages; it isn't a drop-in replacement for it.
As usual, while Joel's overall point is reasonably accurate, most of his supporting points are actually false to the point of being ignorant nonsense. I think someone could tell as good a story in as entertaining a way without stuffing it full of lies, although admittedly my own efforts fall pretty far short.
Notice that you need different data structures.
You can't define anonymous functions in C.
And I wouldn't "attack" Joel for "stuffing the article full of lies". It's more like "abstracting away the details". When people talk of the benefits of some practice or programming paradigm, they don't always mention all the work going into it - they just explain the concept. That's what a good teacher does, IMO. He takes complex concepts and explains the important parts.
You are of course correct. I have corrected the parent post.
My point is, though, that you almost can't define anonymous functions in Python, either, but you can do nearly all of these clever patterns. The difference is that Python functions are closures.
> And I wouldn't "attack" Joel for "stuffing the article full of lies". It's more like "abstracting away the details".
I agree that that's what a good teacher does, but I don't see this article as doing that.
Java is just not very conducive to exploratory programming.
I don't at all think he completely misses the point.
First, anonymous inner classes do capture their lexical environment, and you can access outer variables provided they are declared as final.
Second, even if closures allow another bunch of programming techniques, and even if they seem a natural way of programming with anonymous functions, they are not needed for everything related to being able to pass functions as arguments, at all. Notably, every example joel has given in his article doesn't rely on functions capturing their outer lexical environnment. mapping an add function to an array doesn't, and is still a very useful programming technique.
Admitedly though, i really prefer my languages to support full lexical scoping and closures. But i don't think the article was about that, and i feel more like you missed what joel had to say because you think joel should have introduced the full notion of what a first class function is.
Well, I admit I haven't written much in Java in years, but doesn't that mean they only capture a small part of their lexical environment --- an immutable part? And that means you can't write jQuery.each in Java, right? Any time you want to write a loop with data dependencies between iterations, you need to use some mechanism other than just mutation of variables from the outer scope?
> every example joel has given in his article doesn't rely on functions capturing their outer lexical environnment. mapping an add function to an array doesn't, and is still a very useful programming technique.
You are absolutely correct. Thank you. (That means you can do it in C with almost no trouble.)
> i feel more like you missed what joel had to say because you think joel should have introduced the full notion of what a first class function is.
See my other comment about "nitpicking" for my commentary on that.
They capture only the immutable variables. Wether that's a good or bad thing in itself is open to discussion, but that's clearly not the only problem with anonymous inner classes. Most people end up never using them where you would have passed a function just because it's so long and boilerplatey to do so.
> You are absolutely correct. Thank you. (That means you can do it in C with almost no trouble.)
Yeah that's right, and part of why i consider C being a less retarded language than java.
> See my other comment about "nitpicking" for my commentary on that
I read it, didn't really understand where you were going. You're first saying that joel is incorrect about some details in his article, but i've yet to see which ones. I see him overgeneralizing, sure. The point you then develop is that the fact that he is over-generalizing will harm newbies programmers.
This has been discussed a thousand times before in other contexts, but suffice to say that i profoundly disagree with that kind of statements, and that i find it weak to even go there.
First, you have no real proof of that, so even mentionning it, saying you are sure of it, is weak, you're basically saying "what you're written is gonna harm people", which is a pretty serious accusation in my opinion, without backing it with any facts at all.
Second, i really don't believe that even if it is true and you can prove it, the responsibility should be on the writer. No individual has a perfect knowledge of it's field, that's why it is your responsibility as an engineer to question the facts, and to demand hard data before you say something is true or false. If you fail to do that you have bigger problems than what Joel Splosky is writing on his blog.
In the end, and to explain more generally why i find your critique to be quite unfair, is that, while i agree that Joel is over generalizing, that things are far from being as simple as he is saying they are, the general idea , that i'm gonna sum up as "Having first class functions should be a basic building block of your language" is not only valid, but seriously needs to spread to the whole programming community.
It was because of such an article that i learnt functionnal programming in the first place. It probably wasn't this article, but it wasn't a lot better, maybe even worse. It doesn't mean that i think functionnal programming is the silver bullet to parallelism.
His point was that languages that treat functions as first-class citizens enable a class of abstractions that are tedious/impossible in languages which don't support them.
Whether the naive implementation of map can be easily parallelized across multiple machines/threads/processes is not relevant. In fact, he even says that you need a 'super genius' to write the scaling map and reduce operations.
I think this brings out why I wasn't just "nitpicking." Even though you're presumably pretty savvy, Joel's article has confused you. You're thinking that the easy parallelization is an attribute of the implementation of map, and that a sufficiently smart implementor could make it work.
In fact, the implementation of parallel map is pretty irrelevant here. The important thing is that the function provided to map create no inter-loop dependencies. And given that, you don't need to be a super-genius to implement MapReduce. The super-genius was in coming up with the interface constraints in the first place and recognizing that they were applicable to a large class of problems.
So there are two problems with these "details that are not relevant to the article".
One is that Joel is presenting these details in order to convince you of his point of view. But since they aren't actually true, any confidence they inspire in you is misplaced. You'd be better off with a bald assertion of the importance of functional programming, backed up with "trust me, I know better than you." Admittedly, that's not as much fun to read.
The second problem is that, although Joel has achieved his goal of convincing many people of his point of view, he has done damage to their models of the world in the process. I fully expect to see newbies for years believing that you could never possibly implement MapReduce in C or C++, or that Java requires you to create a whole new file instead of an anonymous function, or that Google Search works by invoking a MapReduce job. And these false beliefs will do real damage to their lives.
Also at the time of this article they had their own custom language Wasabi. This article explaining Wasabi is dated a month after the original article:
http://www.joelonsoftware.com/items/2006/09/01b.html
It's based on VBScript, not C#.
I think MapReduce is really part of a more general pattern where you have an (in more Haskell-y terms) unfold (anamorphism) to a foldr (catamorphism). If your operations on the items in your intermediate set of data in the MapReduce are associative/commutative, you can work out parallelization more or less for free. It's pretty cool stuff, and really not that complicated when you sit down and think about it.
If you are approaching it thinking it is just a map and fold then you are going in disadvantaged and will have to unlearn that fact to properly leverage something that is actually even more impressive/useful/powerful than a mare fold o map.
[1] http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/
[1] http://brendaneich.com/2010/11/paren-free/ -- see the "Implicit Fresh Bindings" section.
It's also hard for me to see how one could have a 'definitive' semantics without having at least one alternative—not that I claimed that either JS's closure semantics were the only ones going. Presumably the alternative you mean is where the variables present in the local environment are copied on creation of a closure.
I'd also resist the idea that I claimed that JavaScript's closure semantics were in some way definitive, although I can see how one might infer that from what I did write. That being said, given the potential problems with the alternative, I would claim that they're a reasonable choice.
Yes, but what are the issues with fresh let-bindings per iteration?
Presumably the alternative you mean is where the variables present in the local environment are copied on creation of a closure.
No, that's bad -- it leaves you unable to show that objects with mutable state are reducible to closures.
fns = {}
for i=1,10 do
fns[i]= function() return i; end
end
for i=1,10 do
print("fns[i]",fns[i]())
end
This produces: fns[i] 1
fns[i] 2
fns[i] 3
fns[i] 4
fns[i] 5
fns[i] 6
fns[i] 7
fns[i] 8
fns[i] 9
fns[i] 10
If you change the set-up loop to look like this, though: local j
for i=1,10 do
j=i
fns[i]= function() return i,j; end
end
...then it prints: fns[i] 1 10
fns[i] 2 10
fns[i] 3 10
fns[i] 4 10
fns[i] 5 10
fns[i] 6 10
fns[i] 7 10
fns[i] 8 10
fns[i] 9 10
fns[i] 10 10
In that case, it has bound the mutable "j" to each of the 10 closures, though you can see it's created a new binding for i on each pass through the loop. It WILL create a new instance of the mutable "j" each time "local j" is executed, though, so if the above code is in a function or another block that's executed more than once, then each time those functions will be bound to a new "j".EDIT: Get the code markup right.
For years I've been doing MapReduce functions, without realising it. MapReduce was in my mental pile of "genius things that cleverer people than me do, must be looked into when there is time."
For info on inject: http://blog.jayfields.com/2008/03/ruby-inject.html
http://weblog.raganwald.com/2007/03/why-why-functional-progr...
[1,2,3].inject{|s,i| s += i}
=> 6
[1,2,3].reduce{|s,i| s += i}
=> 6If it were the other way around, and inject was an alias for reduce, I think there would be a lot less confused rubyists.
What is the history behind the name "inject" here?
(Not so) fun fact: The public MapReduce services by Google and Amazon do not (directly) support JavaScript.
(retract "he's wrong")
Only in really strongly typed languages like Agda can you make the associativity constraint /part of the type/ of the function, and thus statically checked by the compiler; but it's perfectly reasonable to make it part of the specification of the function's behavior that it expects its argument to be associative. (Or, slightly more generally, that the order of reduction is unspecified; in which case only an associative function will produce a totally deterministic result.)
On a related note, I would like to see a "perverse" compiler and library which exhibits random behavior when undefined behavior is specified by the language.
Is lisp really the way to go though?
About two years ago I decided to learn Haskell. Had about 4 false starts and didn't "get" it until my 5th attempt, at which point I figured out what the type system meant and had a bit of a Matrix "I know kung fu!"-moment. Two years of hanging around in #haskell later, I know a billion more things about programming/programming languages and learned type theory (although I still don't grok all conversations there :p).
Some other comments on the usefulness of Haskell:
I started FP with OCaml, but OCaml makes it too easy to continue writing imperative code when you're actively trying to learn new idioms.
I wish Erlang's syntax was more like Haskell, though :\
Bit of a copy and paste fail...
Ruby also has decent fp-semantics, but they are not as clearly cut as in Javascript.
The very fact that Google invented MapReduce, and Microsoft didn't, says something about why Microsoft is still playing catch up trying to get basic search features to work
I don't believe this is true, and that's easy to prove: There was parallelism of SELECTs in SQL Server 2000. So there is a part of MS that is perfectly happy with the idea, even in another bit of MS isn't. They just need to talk more...
def Cook(i1: String, i2: String, f: String => Unit) {
println("get the " + i1)
f(i1)
f(i2)
}
Cook("lobster", "water", x => println("pot " + x))
Cook("chicken", "coconut", x => println("boom " + x))
List(1,2,3).sum
List(1,2,3).mkString
List(1,2,3).map(println) // or more verbose List(1,2,3).map(x => println(x))I took me a while to realize that the person I was explaining it to had only little problems with the templates and compile-time part but close to no idea what a fold or a lambda are.
Not knowing some basics of functional programming can keep a person from understanding so many different things and I have encountered those in different fields (e.g. explicitly like in formal semantics or implicitly in different theories of morphology).
I think the real point here is that different paradigms offer you new views onto the world and enhance your understanding all the programming language things aside.
People are put off by esoteric and academic-sounding names like "first-class functions". But people will adopt it if you put it in a practical context.
haha pretty sure I still do that most of the time...followed by rewriting everything and calling that "refactoring"
The single most mind-opening course I took was functional programming, where I learned LISP and Prolog.
That knowledge today is crucial as it deeply changed my mindset when tackling most any problem.
http://infolab.stanford.edu/~backrub/google.html
Both the URLserver and the crawlers are implemented in Python.
I just built a search engine. I was able to make it massively parallel with some trivial parallel linq code. The power of Linq and the parallel extensions is just amazing.
Also I haven't had an excuse to use it yet but F# seems to have great syntactic sugar for parallelizing things in a more natural way than the typical map reduce.
Explanation: After the 500ms textbox fadeIn finishes it fires a an anonymous callback function which calls a 3000ms fadeOut.
def cook(p1, p2, f)
puts "get the " + p1.to_s
f.call(p1)
f.call(p2)
end
cook( "lobster", "water", lambda {|x| puts "pot " + x })
cook( "chicken", "coconut", lambda {|x| puts "boom " + x })
@a = [1,2,3]
@a.map {|x| puts x*2}
@a.map {|x| puts x}
def sum(a)
@a.reduce(0) do |a, b|
a + b
end
end
def join(a)
@a.reduce("") do |a, b|
a.to_s + b.to_s
end
end
puts "sum " + sum(@a).to_s
puts "join " + join(@a) #!/usr/bin/perl
use Modern::Perl;
use List::Util 'reduce';
sub cook {
my ($i1, $i2, $f) = @_;
say "get the $i1";
$f->($i1);
$f->($i2);
}
cook "lobster", "water", sub { say "pot " . shift };
cook "chicken", "coconut", sub { say "boom " . shift };
my @a = (1, 2, 3);
map { say $_ * 2 } @a;
map { say $_ } @a;
sub my_sum {
reduce { $a + $b } 0, @_;
}
sub my_join {
reduce { $a . $b } "", @_;
}
say "sum " . my_sum(@a);
say "join " . my_join(@a);http://www.slidefinder.net/l/l22_parallel_programming_langua...
Yea, don't look at me like that. My university mostly taught us Java/C++; we only did functional programming in one course.
Umm, yes it can....
#!/usr/bin/perl
sub cook_time { ($hours, $min) = @_; $result = "$hours hours and $min minutes\n"; return $result; }
sub animal { $animal = shift; return $animal; }
sub cook_animal { ($get_animal, $get_time) = @_; return "Cook $get_animal for $get_time"; }
print cook_animal(animal(cow),cook_time(5,23));