The Use Case for Blocks
github.com
github.com
The real issue here is that each() is incorrectly used to perform a search. each() is intended to perform an operation on each member of a set, not query information about the members of the set.
This is not a use case for blocks but an example of an incorrect transformation of the imperative for loop.
jQuery.each() operates on an arbitrary array, .filter() cannot be applied to it. I could have written my own, of course, but doing so would have simply meant wrapping a for loop in a function.
This is a crucial point. The example is NOT bad even if there was a jQuery.select() or jQuery.filter() analogue for arbitrary arrays. The point is that a for loop is a syntactic construct with certain properties, and that you cannot make a direct analogue out of functions. Assuming there was a jQuery.any() function, I could write:
if jQuery.any(collection, function (...) {...}) return 'close';
But I am still moving the return statement out of the block of code. Ruby blocks allow me to leave it where it is.The use case for blocks is when you want to leave the return statement where it is just like in a for loop. The answer of 'use a functional programming construct that works with pure functions' is equivalent to the old Unix answer to any question: 'How do you do X? Don't do X, do Y.'
It is informative and useful to point out that there are elegant ways of not needing a return statement inside a loop. Thanks! But I don't think it invalidates the point that this is where blocks replicate a built-in feature of keywords like 'for' and functions do not.
Unfortunately, it can also result in some serious confusion on the part of a programmer that wants to think of them as lambdas. Java was originally going to do something similar and have two types of lambdas in 1.7, one using => and the other using ==> to indicate the non-local return semantics, and I think it would have confused a ton of people.
If you have a filter that takes a predicate function, for example, you might naively do (excuse my incredibly-rusty-and-thus-totally-syntatically-mangled-Ruby):
some_list.filter() |x| do if (x.foo) return true else return false end
In this case, you'll end up returning from the outer function, NOT just returning a true/false value for filtering. To do that, you have to just do:
some_list.filter() |x| do if (x.foo) true else false end
And rely on the way that things you might otherwise think of as statements are expression values in Ruby. But what if my predicate function is really complicated, and I want to end the block early? Um . . . too bad. Better put that in its own function and call that function from your block.
I'm sure that in practice those aren't major issues for people used to Ruby idioms and behavior, but from someone new to Ruby they can really trip you up.
I am skeptical that this example proved the thesis: "What is there you can't do with lambdas such that you need to add blocks?" unless the point was that you have the "benefit" of performing a goto/return out of a block and not out of a strictly scoped lambda. It seems like a step backward to me. For this example anyway, the better abstraction would be any() -- to treat the set of gestures as a queryable entity and to react to that query.
I haven't played with ruby in years, so forgive my syntax mistakes, but I do not think people would find
i = 0
something_or_other.each do |e|
return i if null(e)
i++
end
to be a better abstraction than return e.length
Come to think of it, you have another return path downstream in that function anyway (not 'close'). return jQuery.any(collection, tester) ?
'close' :
'other value';
I am still open to the possible usefulness of blocks. while (true) {
if (testState()) return valueOfState();
}
moreStateChangingStuff()
as opposed to: var flag = false, result = null;
loop(function() {
if (testState()) {
flag = true
result = valueOfState()
}
})
if (flag) return result
moreStateChangingStuff()Is there a significant benefit to writing this sort of in-between code, or do you think we should pick one or the other whenever possible?
It seems like it adds in a lot of potential confusion and diverges from any other closure/lambda implementation in any other language without adding much value.
foo ifFalse: [^ bar].
doSomething.
Ruby has if and unless and some other control structures with support for returns, but why not allow control structures implemented as higher-order functions to include returns, too?
There are two different use cases here: lambdas (which may be true closures) and user-defined control constructs. In the first case, I'd argue that you want the declaration semantics to be as close as possible to a normal function definition. You should be defining a function, so return returns from that function, and break/continue statements can't have targets outside the function. For control constructs, you want the code to appear as if its inline, so you want non-local return semantics. I.e. you want it to behave as if it's not actually in a separate function at all.
So I think those are two different constructs, and things get confusing when you conflate the two. At the very least if you want to have those two constructs, make them look different enough that users don't confuse them. Java was going to go the wrong way there on their initial closure proposal (the => versus ==> thing) and make things that look like lambdas behave like in-line statements. Ruby kind of goes the other way and makes everything look like a bunch of statements within the body of something else in contexts (like the reduce() method) where what you really want or would expect is a lambda. If I write "return x + y" in the body of my argument to reduce(), I expect that to be the value of the reducer function, not to exit the function that was calling reduce(). But at least in Ruby a block is always a block and always has the same semantics, even if those semantics seem inappropriate for many use cases.
But again, it's personal taste as to which of those, or both, you want to add to a language. In my personal opinion user-defined control statements aren't all that useful and you can fill most of the needs with language-defined control constructs. But it's a tradeoff the language designer has to make, and there's no one right path.
In Clojure (which has no blocks or return statement), I would write that code as
(defn outer []
(first (filter close? something_or_other))
What advantage does the block+return code have over that?Perl <-> Python is my canonical example; I program professionally in Perl and only as a hobby in Python, so the time spent with each language is probably about 5:1 (but still quite substantial with both, as I've been programming with Perl for a long time), so I'm actually more familiar with Perl if anything. Perl is shot through with features, syntactic nuances, and all sorts of magical features both by default and in CPAN, and I even use some of them (selectively), but if it's all so awesome how come I never miss any of it in Python? (And how come the feature I really miss is the ability to use objects as the key for hashes, which Python has and Perl does not? Yes, I know about the CPAN modules, which are hacks and not something I trust, not like Python's built-in support.)
When I look at languages, I want to look at and talk about the global effects the language has on my programming. It is far too easy to get caught up in how awesome this little feature is and how awesome that little feature is and miss what global effect it is having on your code.
Just like with ordered hashes, using hashes with non-string keys needs programming on your part (perhaps overloading stringification for your object and providing a way to get your object back from the string) or others (CPAN). Or you could overload (or just add to) the object and provide an accessor for your value so you can set it on the object itself.
Since I don't know much Ruby I'm not sure if its blocks have other advantages.
Of course, there's also "can we do away with explicit returns". This is what Clamato does to be able to compile a Smalltalk-and-Ruby-like language efficiently into Javascript. You can always abuse exceptions to return early from however far up the stack when you really need to.