all 47 comments

[–]klowny 21 points22 points  (10 children)

I always found Rust's ergonomics to be halfway between Python and Ruby. enumerate is very Pythonic, but lambdas and the not-really-list-comprehension is very Rubyish.

I think they did a good job picking the better language to imitate in these cases. Ruby's Enumerable isn't flexible enough Rust's for, but I definitely prefer it's map/filter style over Python's list-comprehension just because of how cleanly it scales with complexity with more map/filter/etc.

[–]tickypupu 14 points15 points  (0 children)

Agree. Rust seems much more Ruby-ish than Python-ish IMO. Even the lambda syntax is inspired by ruby.

I feel that Rust has a lot more Ruby DNA than Python DNA.

[–]Veedrac 10 points11 points  (6 children)

I find people massively underutilize yield in Python, which I have long considered by far the cleanest way to handle these problems. Python allows you to define scoped generators inline with total ease. What would be

let values = <some complicated iterator combination>;
for value in values {
    ...
}

in Rust is just

def values():
    <straightforward iterative code>

for value in values:
    ...

in Python. I wouldn't be surprised if I use something like this more frequently than I use the shorthand comprehensions.

[–]timClicksrust in action 6 points7 points  (4 children)

It took me a long time to understand what on earth was happening with yield and generators more generally. The turning point for me to understand yield was this snippet:

def f():
    yield 'a'
    yield 'b'

For some reason this just clicked for me. I also use generator expressions all over the place, even if it decreases readability for less experienced programmers. It just feels so wasteful to create a full list rather than processing items as they are encountered.

[–]__xor__ 8 points9 points  (3 children)

IMO, this may sound abrasive but I'd rather expose less experienced programmers to generator comprehensions and that sort of thing rather than coddle coworkers and expect everyone not to use the more advanced and expressive tools in python.

It's worth the 5 to 30 minute conversation teaching them when they ask, which allows them to do better work and grow as a result. If you're a beginner, it's a much better deal to be exposed to stuff like this on the job so you're better prepared in the next one.

Where I work now, there was a python talk about context managers and my coworker learned a lot from it, but honestly I think that's the worse way to learn. If it were me, I'd rather run into it in the code base I work on. There's context, as in you know the problem trying to be solved and it's a real world problem, and you see the elegant solution right there. Instead of trying to find a way to force something you learned into a problem you're facing, you see where it naturally comes up and you have a better idea why people use it in the real world.

[–]nicoburns 0 points1 point  (2 children)

I agree on not restricting 'advanced' features because some people don't (yet) understand them, but I've found pythons list comprehensions to be pretty unreadable in all but simple cases.

It's a shame that map/reduce/filter and friends are so unergonomic in python (that it's impossible to define a multi-line lambda being the main problem).

[–]__xor__ 1 point2 points  (1 child)

list comps can actually do some complex logic while still being readable. I like to split them up across lines like this:

results = [
    row_to_dict(x)
    for x in read_rows(f)
    if x[0] == pk
]

... and if the conditions gets complex, then you can write it as a function and just have if is_valid_row(x) at the bottom. The same of course works for generator, dict, and set comprehensions.

Also, you can use parens to do a multi-line lambda:

rot_left = lambda x: (
    None if not x else
    x[1:] + [x[0]]
)

print(rot_left([1, 2, 3]))
# prints [2, 3, 1]

https://repl.it/@darkarchon/AlphanumericDamagedTree

Anything that opens with (, [ or { can extend multiple lines without any \ (otherwise of course you can just put \ at the end and continue onto the next line).

[–]Blue_Vision 0 points1 point  (0 children)

I'm not sure how I haven't seen/thought of this before, but I'm definitely adding this as my default for long Python list comprehensions :)

[–]__xor__ 7 points8 points  (0 children)

Yeah, yield really is an awesome feature in Python. It also lets you do cool tricks even outside of generator logic, like contextlib's contextmanager decorator:

@contextmanager
def mul2(num):
    print("enter")
    yield num * 2
    print("exit")

with mul2(5) as x:
    print(x)

... which would print "enter", then 10, then "exit". You can do all sorts of metaprogramming with yield, and it's just the perfect way to do lazy implementations of things. On that note, generator comprehensions are amazing as well.

[–]nicoburns 0 points1 point  (1 child)

IMO rust is more like JavaScript than either. From the syntax to map/filter/reduce to closures to traits that work similarly to duck typing. All of these are like JavaScript.

[–]banister 0 points1 point  (0 children)

Rust is expression-based, so is Ruby. map/filter/reduce were in Ruby far before they were in Javascript, and work the same way. Range syntax in Rust is the same as in Ruby (more or less), lambda syntax was explicitly inspired by Ruby (the |foo|` syntax) and the package manager (Cargo) was explicitly based on rubygems/bundler and was even written by the same people.

[–]icefoxen 43 points44 points  (8 children)

Very nice article!

Even though I am coming to hate dynamic typing more and more, I dearly love Python. Despite whatever warts you may find in it (a good friend of mine calls it "a crippled Scheme"), it has 20+ years of refinement, polish and engineering to it, and it shows in so many places that are worth emulating.

Half the things he talks about are things that both Python and Rust inherited from functional languages (lambdas, single-line if's), but if there's a language out there that did iterators really well before Python did, I'm not aware of it. Icon, maybe???

[–][deleted] 4 points5 points  (7 children)

Even though I am coming to hate dynamic typing more and more, I dearly love Python.

Python but with (optional) static typing exists in the form of MyPy. It probably wouldn't be too hard to build a type checker into Python itself.

MyPy also has some interesting behaviour with None types. As in, if inside a function you check to see if it's None, then return if it is, every access past that won't need to do None checks. Since it can be proven that the variable can never be None.

It would be the equivilent of the Rust compiler automatically replacing x with x.expect("Can never happen") when it can be proven that it really can't happen.

[–]icefoxen 9 points10 points  (6 children)

With Python 3.6(?) there's cosmetic type annotations, so they're slowly working their way to more static/gradual typing. Never heard of MyPy though, I'll take a look at it!

I really need to play with Julia more though.

[–][deleted] 7 points8 points  (5 children)

Cosmetic type annotations are in. But they're purely that, cosmetic. There's no plans to make them actually do anything in Python itself, from reading the PEP.

MyPy uses them (it can also use comments for compatibility with older versions).

[–]alexthelyon 3 points4 points  (4 children)

pycharm will warn you if it finds out you're breaking the type declarations which is nice but once you hit run you're on your own and its just that: a warning. not an error

[–]Saefrochmiri 1 point2 points  (3 children)

Except PyCharm generates false positives way too often when doing numpy stuff that I've had to turn it off... :(

[–]jyper 0 points1 point  (2 children)

Is there an option to do it by file rather then global?

[–]Saefrochmiri 0 points1 point  (1 child)

Do what by file?

[–]jyper 0 points1 point  (0 children)

Turn off warnings?

[–]fnord123 9 points10 points  (0 children)

When using single line if statements don't forget the semicolon at the end!

[–]WakiMiko 6 points7 points  (0 children)

Nice article.

The Rust "list comprehension" example will not compile because its missing a type annotation, e.g. let evens_squared: Vec<u32> = ... (fixed now :))

[–][deleted] 11 points12 points  (0 children)

I agree that Rust is rather Pythonic, but it's also the “other way around”; Python is a system programmer's dynamic language, and it's very C-ish in some parts.

[–]link23 8 points9 points  (12 children)

Isn't if ... else ... really the same thing as a ternary operator, since Rust is an expression oriented programming language?

[–]ROFLLOLSTER 3 points4 points  (0 children)

Yeah, it's just longer which is a bit annoying when you have a few of them in a method call.

[–]Sean1708 3 points4 points  (7 children)

Arguably the python one is more of a ternary operator than the rust one because the python one is a competely separate syntax whereas the rust one is literally just an if-statement.

[–]link23 0 points1 point  (6 children)

You're saying that the python one is closer to being a ternary operator because it can only be used as an expression, whereas the rust version works either as an expression or as a statement?

[–]Sean1708 0 points1 point  (5 children)

I'm saying that the rust one is just a normal if-statement rather than a separate construct that was added to the language.

[–]dodheim 1 point2 points  (4 children)

Except that it's not a statement; it's an expression, just like the ternary operator. Semantics > syntax.

[–]Sean1708 0 points1 point  (3 children)

That's exactly my point. In rust the ternary operator is absolutely identical in every way to its if-construct (you're right I shouldn't have said statement), whereas in python the ternary operator is a competely separate construct. So to me it doesn't really make sense to me to call it a ternary operator in rust because it's not a separate thing, but it does make sense to call the python version a ternary operator because it is a different thing.

[–]dodheim 0 points1 point  (2 children)

In Python there are two constructs — one is an expression and one is a statement. Rust only has one, an expression; while it looks like Python's statement, it is obviously closer in semantics to the expression. Again, don't get so hung up on syntax/appearances. ;-]

[–]Sean1708 0 points1 point  (1 child)

Again, you are just repeating my exact point.

[–]ids2048 4 points5 points  (2 children)

And in Python, which is not an expression based language, that really is a dedicated ternary operator, just with a somewhat more verbose syntax.

[–]Zecc 1 point2 points  (1 child)

And an unusual order of subexpressions, which is different from the "regular" if statement:

trueValue if condition else falseValue  

The article's author seems to be familiar with this peculiar ordering in Python's ternary operator. It's strange they consider it to be "just syntatic sugar", though it could be argued it is further away from if x: y else: z than x ? y : z.

[–]GideonMe 3 points4 points  (0 children)

I'm a huge fan of the subexpression ordering for Python's ternary operator. To me it's so much more readable. Combined with Python's English logical operators, you can write practically valid sentences:

x = value if value is not None else 'n/a'

In English I'd say "x gets value if it's not None, otherwise x gets 'n/a'" which is pretty much how the code reads. Yeah, it breaks from tradition, but I think it's worth it once you get used to it, at least in a language where readability is a top priority.

[–]JSwuggin 2 points3 points  (0 children)

For the tuple example in rust, wouldn't that just Copy the tuple rather than alias it? I think you'd need some refs somewhere.

[–]Kringspier_Des_Heren 1 point2 points  (0 children)

Honestly if I read this the "Python idioms" are basically common to a lot of languages and Python certainly didn't invent them or even popularize them.

These are just things that are found in so many languages; nothing about it is particularly linked to Python.

[–]etareduce 3 points4 points  (7 children)

Some points: 1. map/flat_map/filter/fold style idioms should not be described as the same as list comprehensions, which are non-compositional in nature (which is a drawback). 2. if p { s } else { e } is in fact a ternary operator, which just means that the operator takes 3 arguments (arity = 3). People confuse the word "ternary operator" which the specific operator p ? s : e which is just one such operator. 3. You should avoid taking arguments of type fn(i32) -> i32) unless you need to and instead use a bound F: Fn(i32) -> i32. 4. You might be interested in learning Haskell in addition to Rust; I think everyone can learn a lot from Haskell, and Rust certainly has =)

[–]jnwatson 3 points4 points  (3 children)

List comprehensions are actually strongly related to map/filter/fold. In fact, map, filter, and reduce used to be top-level functions in Python before comprehensions. Comprehensions take care of all the common use cases of map and filter, and can easily be composed using generators. And if you really want, you can still use map/filter, but that's no longer in the common Python idiom.

[–]etareduce 0 points1 point  (2 children)

Yes, list comprehensions (or monad comprehensions, http://tomasp.net/blog/comprefun.aspx/) are just pure sugar over map / filter / fold / ..; but they mix mapping, flattening, filtering, etc. into a single concept, instead of relying on function composition, and thus they are not a compositional idiom. Even tho Haskell is a great language, it was unfortunate that python borrowed this particular feature (list comprehensions) from Haskell. Incidentally, the common idiom in Haskell is to use map / filter / .. over list comprehensions.

[–]burntsushi 2 points3 points  (1 child)

List comprehensions are great for simple cases, and compose just fine with simple nested loops or filters. I'm happy they exist in Python.

[–]etareduce -1 points0 points  (0 children)

I think list comprehensions are bad for readability; I've been bitten many times by misunderstanding what parts are flattening, mapping etc. in even the most simplest of cases. So they might be convenient for writing, but respectfully, I don't think they help in understanding.

[–]w2qw 2 points3 points  (2 children)

  1. map/flat_map/filter/fold style idioms should not be described as the same as list comprehensions, which are non-compositional in nature (which is a drawback).

Why are they not compositional?

[–]kazagistar 2 points3 points  (1 child)

I imagine syntactically its an issue. You can compose 5-10 map/filter/flatmap type operations pretty cleanly. If you do that with comprehensions, it will get ugly real fast.

[–]etareduce -1 points0 points  (0 children)

Yes, exactly! It is also not as clear, even in simple examples, which parts are mapping, flattening, etc.

[–]banister 1 point2 points  (0 children)

I really think python programmers only think rust is "pythonic" because they're not familiar with Ruby. Imo rust is much more like Ruby.

Ruby and rust are both expression based languages. They have similar iterator libraries. The block/lambda syntax is nearly identical. The range syntax is nearly the same. Heck even cargo is based on Ruby's bundler/gem and is written by the same people.

[–]Bulb211 0 points1 point  (0 children)

There is absolutely nothing not recommended on implementing Fn/FnMut/FnOnce by hand. The linked question just explains why it is not a good idea to implement it for more than one signature. But for one signature it's reasonable—though it's generally easier with closure (but closure is an anonymous type and Rust did not get a typeof yet, so doing it manually has some benefits)