Hacker News new | ask | show | jobs
by bpicolo 3743 days ago
List comprehensions are awesome. Not only that - python does them insanely beautifully. Clojure and ES6 are examples that I think aren't as readable, though equally powerful more or less. But in python they don't have any clunkiness. Simple, expressive. Love them.

Nesting them can get ugly, but it's easy to avoid: Just use generator expressions and chain them. No real runtime overhead that way.

1 comments

Well with regards to clunkiness and awesomeness, are they not still less 'naturally' composable and (as a result) less readable in composition than collection pipelines? I don't see a good reason to prefer them over:

    collection
        .map(x => x * 2)
        ...
        .filter(isOdd)
        .reduce(blargh)
... style syntax that most other modern, C-style languages offer now (ES6, Rust, Ruby, C# ...)
The comprehension approach, while requiring more syntax, seems to often produce a more 'natural' order of operations. For example, compare

  [p for p in range(2,100) if all(p%q!=0 for q in range(2,p))]
with

  range(2,100).filter(p => range(2,p).map(q => p%q!=0).all())
It might be a small thing, but in the first one I feel the prime 'p' becomes the center piece, whereas in the second, 'p' is burrowed somewhat in the expression.
Both are fairly unintelligible to me (probably partly due to my lack of interest in prime numbers), so it seems like splitting hairs to draw comparisons, but if I had to figure out what was going on, I'd rather be staring at this (~ES6):

    range(2, 100).filter(p => {
  
        // I'd probably stick a comment here to explain this ...
        // I'd split out this whole section into a named function once I understood it.
        return range(2, p)
            .map(q => p % q != 0)
            .all();

    })
I don't feel like I could iterate on understanding this problem so well in Python because I have to reach for one of 2 seemingly inferior solutions (list comprehensions or borked lambdas).
I think there are values in both styles. I've written a lot of both and for simpler things (which the vast majority if not all of code should be), I definitely prefer list comprehensions. They feel even more functional because they don't depend on map/filter/reduce defined on your objects.

I think chaining syntax from e.g. Elixir gives you more flexibility than map/filter/reduce defined on objects too. Big fan of that, though it does interact oddly with elixirs optional parentheses for function calls (which is a mistake of the language imo)

This is nothing new at all, it is called the "Fluent Interface" design. Many Python libraries implement this design, for instance the Pandas library is a popular example.

I hate this design approach deep in my soul. It makes for very brittle code that creates lots of backward compatibility issues. If you're working on some legacy code that has some nonsense like

    foo.get_status().dispatch_handler().log_error().close()
it is maddening! You have to untangle just what exactly gets returned by every step of the chain, so that you can ensure you're in the right context to know exactly what the next call of the chain is doing.

In that example, say someone changes `foo.get_status()` to return some new kind of "status" object, and it alters the `dispatch_handler` and so on. Of course one can implement this in a way where the chain of downstream calls doesn't break, but the point isn't so much that, through huge engineering effort it is possible, but rather that it is extremely brittle and adds a layer of complexity that's not needed.

It's just so much better to write something like:

    dispatch_result = run_dispatcher(foo.get_status())
    log_error(dispatch_result)
When the intermediate points of the chain are just functions, instead of member functions of a class, it means you can easily experiment with them and figure out what's going on without needing to recreate the entire set of context along the whole chain.

`run_dispatcher` in my example would be a hell of a lot easier to unit test and throw some mocked example class into for debugging or refactoring than if it is `some_class.run_dispatcher` ... and then if `some_class` has child classes that specialize the behavior, you're just hosed.

The problem is composability. People think that the fluent interface makes things composable because from some arbitrary point in the middle of the chain of calls, they have easy attribute-like access to the next operation they want to do. This artificially feels easy and convenient.

But contrast this to a functional language like Haskell, where none of these things need to be member functions of an object, and hence the context of the object doesn't have to be created at any point in the fluent chain. Then you can write something even better:

    (close . logError . dispatchHandler . getStatus) foo
We can even easily refer to this whole chain of events with a single function name:

    let statusDispatchLog = (close . logError . dispatchHandler . getStatus)
(And, of course, we get lots of nice type checking in statically typed languages to ensure that the composition actually makes sense -- which not only protects you at run time, but is also a huge help to clue you in to your design flaws. If you're trying to shoehorn some stuff into a fluent interface and it's not working, it probably means you have thought clearly about how the methods should "flow" in the call chain.)

To do the same thing in a fluent interface, we need a horrible lambda or a whole new function definition, exactly because the fluent interface is only sweeping the composability issues under the rug.

    statusDispatchLog = lambda x: x.get_status().dispatch_handler().log_error().close()
The difference is subtle, but important. Instead of making a new function that is explicitly the composition of other functions, you are making a function that just happens to access other functions as attributes, and if you set it up correctly then it acts as a sequence of composition.

In Python this is particularly a shame because functions are first class objects. Of course, you can write helper functions / decorators that sort of do function composition (if you're willing to throw away useful argument signatures), or you can use flaky hacks like the common Infix pattern in Python, and then live with ugly "<< . >>" or "|.|" misleading syntax.

It always makes me sad that Python lacks an extremely short function composition infix operator that provides some information about the function signatures of the functions being composed.

Because not even a comprehension can help you when you need to do the fluent interface stuff in Python.

    [x.h().f().g() for x in some_iterator]
This is so much worse than

    map(g.f.h, someIterator)
or

    [(g.f.h) x | x <- someIterator]
or even

    [g(f(h(x))) for x in some_iterator]