(filter (map expensive_call X) cond)
So do list comprehensions: [y for y in [expensive_call(x) for x in X] if cond(y)]
Near as I can tell, the only difference is that list comprehensions also provide a syntactic sugar for the convenience function filter_then_map. Sometimes this saves you a level of nesting, sometimes not.Incidentally, this isn't even an issue in a pure functional language with sufficiently smart compiler.
Plus, you get 5 mentions (3 y's and 2 x's) of some intermediate variables instead of 0 in your code, so both token-wise and char-wise, map/filter alternative is shorter, and less a mental burden (think about the "succinct" idea by PG).
If you can put together a nontrivial usage of map+filter with at least three source collections that's more concise than the equivalent list comprehension I'll (figuratively) eat my hat.
map(lambda (w,s,l): {'widget': w, 'sprocket': s, 'location': l}
filter(lambda (w,s,l): l.hasInStock(w) and l.hasInStock(s) and w.isUsableWith(s),
product(widgets, sprockets, locations)))
Well, I agree it is not any conciser that its list comprehension (about the same I guess?). Nothing is perfect, like you said, know you tool :)The argument in favor of set-notation being higher level is it's less specific (it doesn't explicitly provide a sequence of operations, just an outcome).
List comprehensions look like set notation but have an implicit procedural translation you have to keep in mind to use them well, so it's a toss-up.
I prefer map/filter/reduce when sequencing has large performance implications but for simple filtering or raw-data-shaping comprehensions read more smoothly.
Now you know why many people like me are gradually pissed off by Python and start the exile ... currently trying Scala and it seems a nice language (with list comprehension too! :)
> List comprehensions look like set notation but have an implicit procedural translation you have to keep in mind to use them well, so it's a toss-up.
Actually I think that's the problem I have with list comprehensions: I use them a lot in my code, usually 1~3 levels nested and then have a hard time tracking down the order of implicit loops (which is inner vs outer) and make sure the intermediate variables (x for x in y for y in z ...) do not clash ... OK maybe I'm using it too much and in the wrong way :(
> I prefer map/filter/reduce when sequencing has large performance implications but for simple filtering or raw-data-shaping comprehensions read more smoothly.
I didn't know map/filter is faster than list comprehensions? I thought both are optimized by Python interpreter. But I like the idea of knowing that at least map can be parallelized easily. But since Python does not utilize multicore in a decent way, all bets are off :(
do { w <- widgets;
s <- sprockets;
l <- locations;
guard (l `hasInStock` w);
guard (l `hasInStock` s);
guard (w `isUsableWith` s);
return (w, s, l); }
We're trading horizontal space for vertical space. I think it's much clearer than either list comprehensions or plain map/filters. It's the best of both worlds.If eg I edited it to be:
do { l <- locations;
w <- widgets;
guard (l `hasInStock` w);
s <- sprockets;
guard (l `hasInStock` s);
guard (w `isUsableWith` s);
return (w,s,l); }
Does that force it to go through "location-first" and only check the w and s of (w,s,l) for compatibility if it has already ascertained that w and s are in stock @ l?List comprehensions: the set contains this.
Map/filter: apply these functions to the set.
Procedural: follow this recipe for turning one list into another.
[fn(x) for x in xs for xs in xss if cond(xs)]
I always think it's a loop, then an inner loop (and loops -> procedure ... then get confused which is inner and which is outer :| filtered = [x for x in seq if x>10]
Python's list comprehension is much more readable than using map/filter/reduce - at least for Python programmers :) Anyhow, I really like Guido's decision on dropping these - it creates a cleaner language and forces people to think Pythonic when programming in Python.For simple cases, yes. But I wouldn't say
[each for each in [expensive_call(x) for x in seq] if cond(each)]
is more readable than filter(cond, map(expensive_call, seq))
at least for functional-thinking minds. The level of thinking in abstract is different here.Now the problem is, some people see Python as a very functional language (with first-class functions etc) and want to use it that way (like Lisp), but BDFL and some core Python devs believe it is better to keep it Pythonic, thus those functional people are kinda pissed off by this and switch away from Python.
Personally I don't think it will make Python a lot cleaner to remove two auxiliary functions and force people to use list comprehension when it is completely trivial to add these missing pair back (two lines of code).
(disclosure: I prefer FP, but I also think keeping things Pythonic is fine most of the time. It's just that in this case, I think map/filter is pretty "Pythonic" according to me. :)
Later, I ran into some cases where I wanted a multi-line lambda. And my thought was "aaragh134!#?!"
Then, a weird thing happened. I started making a conscious effort to follow PEP 8. 79 column limit? Seriously? That sucks. But after weeks of struggling with it, something finally hit me. I realized I was writing better code by forcing myself to reduce code density. Sure, it was a little bit longer, but I spend way more time reading it than writing it.
Stop trying to fight it; assign a name to that lambda. Readability counts.
Stop trying to be clever; assign a name to that inner expression. Flat is better than nested.
De-emphasizing functional operations makes it more likely for libraries to work in a non-functional style, for tutorials to do so, etc.
It's tiring fighting against a community and a (benevolent) dictator that disagree with you.
That summarizes the problem I guess :D
Actually only one line of code is enough for each of map/filter:
def map(fn, seq): return [fn(each) for each in seq]
def filter(cond: seq): return [each for each in seq if cond(each)]
But seriously, what do you really gain by removing these two? Isn't that too ideological? I don't really see how un-Pythonic it would be to use map/filter instead of list comprehensions. The problem is BDFL's attitude seems to drive many FP-ers away, like the guy in the original post. (filter #(> x 10) seq)
sure is readable for me, and I'm fluent in Python and various Lisps. (That example is Clojure.)I would like to point out, however, that CL allows you to write:
(loop for x in seq
when (> x 10)
collect x)
which you might think is verbose ("why do I need that 'collect'?")... except that loop allows you to write things like (loop for i in *random*
counting (evenp i) into evens
counting (oddp i) into odds
summing i into total
maximizing i into max
minimizing i into min
finally (return (list min max total evens odds)))
Loop knocks Python's trivial list comprehensions into a cocked hat.I switch between map/filter and loop depending on whether I'm working with predefined functions (e.g., (filter 'less-than-ten seq)), handling multiple sequences, doing side-effects, etc.
CL could really use something like Python's generators though. The loop macro is an ugly hack in comparison.
The question is whether "Pythonic," as the community defines it today, is optimal in all cases. As an analogue, there have been enough talks about things the Java community considers to be stylistically optimal that look really horrible compared to implementations in other languages -- like Python. :-) Pythonic style should be a guide, not an edict, and should be deviated from or redefined when it makes sense. The examples riobard provided already show the syntax weighing things down, a situation where syntax should give way to a more functional style approach.
As for it creating a cleaner language, I try to approach this, as with all things, from the perspective of being an language-agnostic programmer. From that perspective, I do like the python syntax for simple things like what you defined, but under heavier weight mapping and filtering operations, the map/filter function call syntax seems a lot cleaner. There's nothing wrong with syntactic sugar, but I would assert that it will tend to suck when it is all you have.
You can arguably trace Python's list comprehension syntax all the way back to setl, a "set-theoretic programming language", and the resemblance to mathematical set-builder notation is intentional; compare
- let B = { g(a) s.t. | a \in A and f(a) holds}
- B = [g(a) for a in A if f(a)]
If you're used to thinking in sets having to decompose into "maps" and "filters" is a speedbump; easy to do but nice to avoid.
Where list comprehensions really start to shine is making it comparatively trivial to pull from multiple source collections without a lot of ugly machinery:
[{'widget':w,'sprocket':s,'location':l} for w in widgets for s in sprockets for l in locations if l.hasInStock(w) and l.hasInStock(s) and w.isUsableWith(s)]
...which is about where explicit map + filter start to become annoying. You can use: map(lambda i: {'widget':i[0], 'sprocket':i[1], 'location':i[2]}, filter(lambda i: i[2].hasInStock(i[0]) and i[2].hasInStock(i[1]) and i[0].isUsableWith(i[1]), itertools.product(widgets,sprockets,locations)))
...but to my eyes that is not only very ugly but just going by character count the # of characters given over to keywords (map, lambda, filter) instead of "what i'm doing here" is huge. Additionally use of itertools forces use of tuples for your intermediate values and thus the lambdas are gobbledegook until you get to the tail end of the statement and see that i[0] == widget, i[1] == sprocket, and i[2] == location. I could define some constants (WIDGET = 0, SPROCKET = 1, LOCATION = 2, etc) but now it's even longer.It gets even worse if you try to be clever with the sequence of operations.
You might look at that definition and say lo! I can pre-filter out stuff not in stock at each location and make things more efficient. Naively you'd wind up with:
map(lambda d: {'location':d[0], 'widget':d[1], 'sprocket':d[2]}, flatten(map(lambda l: list(filter(lambda p: p[1].isUsableWith(p[2]), itertools.product([l],filter(lambda w: l.hasInStock(w), widgets), filter(lambda s: l.hasInStock(s), sprockets)))), locations))) #nb must-supply-you-own-flatten-method
Under some circumstances that might be substantially faster than the previous approach. But compare the equivalent but you could have instead gone with: [{'widget':w,'sprocket':s,'location':l} for l in locations for w in [widget for widget in widgets if l.hasInStock(widget)] for s in [sprocket for sprocket in sprockets if l.hasInStock(sprocket)] if w.isUsableWith(s)]
So you lose a little flexibility in simple cases at in exchange for increasing the scope of what you can get away with as "readable" one-liners.Level 0: for-loops with an explicit accumulator
Level 1: map + filter (!)
Level 2: ??? arguably an atemporal set-theoretic approach
In practice in python list comprehensions are a superior syntax for computing with multiple source collections.
(!) Really all you need is reduce
map = lambda f,l: reduce(lambda h,t: h + f(t), l, [])
filter = lambda f,l: reduce(lambda h,t: h + t if f(t) else h, l, [])
You'd be silly to implement them that way of course but know your tools.A saner map/filter/product version:
map(lambda (w,s,l): {'widget': w, 'sprocket': s, 'location': l}
filter(lambda (w,s,l): l.hasInStock(w) and l.hasInStock(s) and w.isUsableWith(s),
product(widgets, sprockets, locations)))
e.g.
equals but equals note because of "expensive_call", it's inefficient (and silly) to do So map/filter combination gives more flexibility than list comprehension, and for functional-thinking minds, it's just so natural to think in abstract terms of passing functions around. List comprehension is pretty syntactical sugar to do similar things, but it forces you think about the "how to do" instead of "what to do".That said, it's not really "compelling" though -- there is no real "compelling" reason to switch from one Turing-complete language to another given that you can do the same thing eventually. But hey, it's the itches that drive us nuts, isn't it? :)
Just my 2 cents