I took some of the code I talked about struggling with in my last post, and set about translating it into Python. I wanted to see if the bugs were more obvious in a language I was more familiar with. Unfortunately before I was finished with the translation, my machine died and refused to restart, complaining about a disk error... so I may well have lost all my work. (ARRRGH!!) Anyway, before that happened, I was getting the feeling that the bugs weren't very obvious, even in a language I knew better.
What I did find though, was that it was quite hard to make the methods as short in Python as they were in Ruby. I could make them as short, the language has the necessary features to do it, but when I did so, they just stopped looking like good Python to me.
The other thing that I found was that RSpec lets you write really readable and well organized tests. Translating them into unittest made them just a travesty of their former selves.
I've just listened to this talk by Gary Bernhardt, who is experienced in both Python and Ruby, and who has clearly thought quite hard about these kinds of issues and knows both languages very well. He has even tried to write a RSpec clone for Python, called Mote. (He says himself that it isn't as good as RSpec, and that it is because Python lacks blocks, and won't let him monkeypatch core classes).
Anyway, about half way through the talk, Gary shows this code example, first in Python:
'\n'.join(obj.name
for obj in (
repository.retrieve(id)
for id in ids)
if obj)
Then the equivalent code in Ruby:
ids.map do |id|
repository.retrieve(id)
end.compact.map do |obj|
obj.name
end.join('\n')
Gary makes the point that the Ruby code is easier to read - you can follow it from top to bottom and see what it does. The thing is, I don't think many people would write code like that in Python. I might write it more like this:
objs = [repository.retrieve(id) for id in ids]
objs = filter(lambda x: x, objs)
names = [obj.name for obj in objs]
'\n'.join(names)
This code is four statements long, rather than one, and has two local variables ("objs" and "names") which the other two code snippets lack. In real code, you would probably be able to come up with rather more descriptive names, drawn from the problem domain. When I compare this code with the Ruby, I don't think it is any less readable. The filter(lambda x: x, objs) is not as nice as the Ruby call to "compact", but on the other hand, I think the two additional local variables make it clearer what is going on.
I'm wondering whether the trouble I was having locating bugs in these small methods was because they were cramming so much into one statement, and almost completely lacking in local variables. That seems to be the Ruby way of doing things - maybe I will just get used to it and learn to read it just as well eventually? I guess I am going to find out!
Anyway, I'm really hoping the friendly support technicians manage to save the contents of my hard disk, I want to use the code in an exercise at the upcoming Gothenburg Python Conference - GothPyCon. I am hoping to run a workshop where half the room gets the buggy code with small methods, and the other half gets the same code refactored into longer methods. You get half an hour to find the bugs, then swap to the other codebase and find them again. Then I was hoping to have a discussion about which codebase was easier to debug, and/or split into pairs and re-implement the code from scratch, and see if we could come up with other designs that solved the problem more elegantly and transparently.
If issues like this interest you - design, refactoring and testing -, I really hope you will come along!
2 comments:
This comment was left by Leo Soto, and I unfortunately managed to click the wrong button and delete it. I'm so sorry Leo! Here is the comment, anyway.
Hi Emily,
Nice to see another Hashrocket related person also active in the Python community :).
Personally, I still miss Python's explicitness (or maybe it's not just about the language but also the culture around it?) when hacking Ruby code.
I think I'm at the point in which I enjoy Ruby more than Python for writing code. Also for "high-level" reading. But for really understanding/debugging code, those small things like doing too much in one expression or not having a clear idea where such or such method comes from definitely makes my life harder than what I think it should be.
From my POV, the problem is: In general, writing code is more enjoyable than reading code. But in practice, we spend way more time reading code than writing it. It's not an easy trade-off.
An experienced rubyist would probably have written it like this.
ids.map { |id| repository.retrieve(id) }.compact.map(&:name).join("\n")
It uses symbol to proc which you can get from activesupport (support library that is part of RubyOnRails but can be used for other things) or Ruby 1.9.
Single line blocks and chaining are usually done with {} instead of "do" and "end".
Post a Comment