Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I want to focus on one point that I find an interesting study in language design:

> * I haven't found an expression that is better written as a list comprehension than as a map/reduce/select expression.

map/select are better iff you have a concise block syntax.

For mapping with a single ready function, all styles are reasonable:

    items.map foo
    items.map { |x| foo(x) }
    map(foo, items)
    [foo(i) for i in items]
Now suppose you want to modify the mapping function to foo(bar(i)). You could cascade it:

    items.map(foo).map(bar)
    map(foo, map(bar, items))
but this quickly gets out of hand and makes modifications painful. What you really want is a place to write arbitrary expressions instead of just a function name.

With comprehensions OR a good block syntax, it's a smooth transition:

    [foo(bar(i)) for i in items)]
    items.map { |i| foo(bar(i)) }
But without one, the change is ugly and annoying:

    map(lambda i: foo(bar(i)), items)

So why wouldn't Python just embrace a nice block syntax? Precisely because this naturally leads to expressing most control structures as functions taking blocks!

Now that is not wrong in itself, but it is a question of taste. Pythonic taste favors a different approach: instead of passing the block directly to a function, have a few built-in control structures that can act as combinators.

E.g. don't write an "each" method that accepts a block - write an iterator, and use the built-in "for" loop as a bridge between the iterator and the block. Why would this be any better?

1. Because we can: it turns out that the vast majority of custom control structures can be sorted into a few patterns (iteration, pre/post guards, function wrapping), each served by one natural combinator ("for", "with", decorators). There is a price: it takes time until new patterns are recognized and the missing combinator is added (e.g. "with" is a very recent addition).

2. Decoupling: an iterator is a self-contained passive object that can be used outside of a for loop. An "each" method takes control and is less versatile.

3. Readability: seeing the combinator gives you an immediate idea about the style of flow control. This is just a matter of taste (Lisp and Ruby solve this with naming conventions).

4. Syntax taste: stupid as it sounds, passing blocks to functions is just alien to Python's syntax. Many smart people tried to marry them - and it wasn't a pretty sight...

But in the end you can't judge decisions of taste by pro/con arguments. You have to look at the sweet spots (and the sour spots) that arise in practice. A particularly nice sweet spot that arose in Python is the combination of reduction functions with generator expressions:

    sum(i**2 for i in range(10))
    any(p % i == 0 for i in range(2, p - 1))
    dict((v, k) for (k, v) in dict_to_invert)
This is very cool because the map-reduce pattern covers lots of useful computations. Of course it's just a one case and YMMV. I'm sure Ruby has its own sweet spots...


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

Search: