The Artima Developer Community
Sponsored Link

Weblogs Forum
The Adventures of a Pythonista in Rubyland/2 - No Comprehension

30 replies on 3 pages. Most recent reply: Jan 4, 2009 7:26 PM by Carl Graff

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 30 replies on 3 pages [ 1 2 3 | » ]
Andy Dent

Posts: 165
Nickname: andydent
Registered: Nov, 2005

The Adventures of a Pythonista in Rubyland/2 - No Comprehension (View in Weblogs)
Posted: Oct 27, 2008 1:52 PM
Reply to this message Reply
Summary
Trying to do some simple file processing and strip blank lines, Andy stumbles through the "Ruby is supposed to feel natural" approach and just tries things. The conclusion - Python list comprehensions are still more flexible and powerful than any Ruby feature and inject is possibly the most misleadingly named function I've ever encountered.
Advertisement

Ruby gurus, be gentle with me and please correct any misunderstandings below and in the rest of this series!

Stripping Blank Lines

I want to simply build a list of strings with the empty strings removed.

The following examples are taken directly from the Python and irb interactive interpreters. The >>> are standard Python interpreter prompts, showing what I typed, with responses lacking any prefix. The Ruby irb interpreter is similar with >> prefixing my entries and => prefixing the interpreter responses.

The generator-based approach in Python is very compact:

>>> aList = ["fred", " ", "harry"]
>>> [x for x in aList if len(x.strip()) > 0]
['fred', 'harry']

So I first tried an iterator block with trailing conditional logic - it felt natural to write, pity it didn't work:

>> aList = ["fred", " ", "harry"]
=> ["fred", " ", "harry"]
>> aList.each { |x| x unless x.rstrip.empty? }
=> ["fred", " ", "harry"]

it appears that each just returns the original object.

Although it didn't work, this attempt provides a useful jumping-off point to discuss what I think is the single most powerful idiom in Ruby and how it is implemented - yield and the optional code block.

Just one quite note on container types, before we move on - Ruby arrays are equivalent to Python lists, hashes to dictionaries and there's no equivalent of the immutable Python tuples.

Any method in Ruby can have a code block argument attached to the parameters, some methods like each require such a block. The role of yield inside a method seems identical to that in Python generators. The difference is in the recipient of the yield and the resulting programming pattern. One way of looking at it would be to say Ruby yield is inside-out compared to Python yield.

Python yield is used to return control with one or more values to the calling context, which is then in charge and makes a decision as to whether to invoke the yielding function again. There is no obligation on the calling code to keep calling the generator which yielded, even if there are values in a container just begging their turn to be iterated.

Ruby yield is used to pass control and one or more values to a code block that has been sent to a method. Your yielding method is in charge and when the code block exits, control returns automatically to the yielder.

Yield is the Strategy Pattern provided in the core of the language.

For example, the open method can take a block and return the results of the block. You can write some code like a typical Python idiom:

>>> to_clean = open("test.txt").readlines()

identically in Ruby:

>> to_clean = open("test.txt").readlines()

or in Ruby with a block:

>> to_clean = open("test.txt") { |f| f.readlines() }

The block-based Ruby version is more predictable and safer. If there is a failure within the block, it guarantees the file closure. Blocks in Ruby have their own scope and are also closures - they retain the context for variables from outside the block which are used inside the block. Blocks in Smalltalk are typically used to pass logic into other method calls. With the yield style of using blocks in Ruby, they seem to have a more local purpose.

The declaration of Ruby blocks is very similar to Smalltalk blocks and straightforward. Blocks are bounded by the words do and end or a pair of braces. The Ruby convention that has evolved is to use braces for single-line examples as I've done here and use do/end for bounding multiple-line blocks.

The vertical bar or pipe character is used to bound the parameters to the block. These parameters are populated by whatever is yielding to the block so their purpose and count depends on the method you're sending the block to. The comma-separated list of parameter names is just names, no typing information of course!

Recipe 7.2 in Ruby Cookbook has a nice detailed explanation of yielding to blocks and subsequent recipes show more examples iterating over data structures.

Back to Our Blanks

The each method returns the container, as all Ruby expressions return a value. If you do something like printing the parameters to the block, you will see them printed first:

>> aList = ["fred", " ", "harry"]
=> ["fred", " ", "harry"]
>> aList.each { |x| print x }
fred harry=> ["fred", " ", "harry"]

Looking through the definition of the Enumerable mixin (more on what mixin means later), I spotted the collect method which does create a new array from the results of the block:

>> aList = ["fred", " ", "harry"]
=> ["fred", " ", "harry"]
>> aList.collect { |x| x.length unless x.rstrip.empty? }
=> [4, nil, 5]

Oops: it is very literal-minded - if you don't return anything in the block, a nil is put in that slot in the array being created.

Enough elegance - rather than trying to write an expression which returns the data I want, I gave up and decided to have the code block act on some data storage from outside that was pre-declared:

>> destList = []
=> []
>> aList = ["fred", " ", "harry"]
=> ["fred", " ", "harry"]
>> aList.each { |x| destList << x unless x.rstrip.empty? }
=> ["fred", " ", "harry"]
>> destList
=> ["fred", "harry"]

More on blocks in a future episode, this one posted from Hokitika on the West Coast of New Zealand's South island where we are enjoying a holiday.


Dustin Sallings

Posts: 2
Nickname: dlsspy
Registered: Sep, 2006

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 27, 2008 2:36 PM
Reply to this message Reply
How about

aList.select {|x| x.strip().length > 0}

Steven Hansen

Posts: 2
Nickname: stevenwulf
Registered: Feb, 2005

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 27, 2008 6:53 PM
Reply to this message Reply
aList.select { |i| !i.strip.empty? }

And yes, inject is the most horribly missed named method ever. Why they didn't use accumulate (the scheme equivalent) continually eludes me.

Steven Hansen

Posts: 2
Nickname: stevenwulf
Registered: Feb, 2005

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 27, 2008 6:56 PM
Reply to this message Reply
Whoops, I didn't see you post Dustin.

Michele Simionato

Posts: 222
Nickname: micheles
Registered: Jun, 2008

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 27, 2008 9:30 PM
Reply to this message Reply
> [x for x in aList if len(x.strip()) > 0][code]

I would write that

[code][x for x in aList if x.strip()][code]

Antti Tuomi

Posts: 4
Nickname: hrnt
Registered: Dec, 2006

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 28, 2008 2:17 AM
Reply to this message Reply
> aList.select { |i| !i.strip.empty? }
>
> And yes, inject is the most horribly missed named method
> ever. Why they didn't use accumulate (the scheme
> equivalent) continually eludes me.

In my opinion there is not a good name for that function -
it is not used to simply add stuff together, for example:

Find the largest element of an array
[2, 1, 5, 4].inject(0) { |a, b| a > b ? a : b }

Check that all elements are true
[1, "foo", nil, "bar", 5].inject(true) { |a, b| a && b }

And so on. By the way, Python calls it "reduce". The function is also commonly called a left fold.

I believe the Ruby name comes from Smalltalk.

collin miller

Posts: 2
Nickname: collintmil
Registered: Oct, 2008

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 28, 2008 4:49 AM
Reply to this message Reply

["Paul", "", George].reject{ |name| name.blank? }

collin miller

Posts: 2
Nickname: collintmil
Registered: Oct, 2008

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 28, 2008 4:53 AM
Reply to this message Reply
er..


["Paul", "", "George"].reject{ |name| name.empty? }


Or for extra points


class Symbol
def to_proc
Proc.new { |obj, *args| obj.send(self, *args) }
end
end

["Paul", "", "George"].reject &:empty?


Though that will slow you down and cause some arguments between ruby programmers.

Philip Brocoum

Posts: 1
Nickname: stedwick
Registered: Oct, 2008

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 28, 2008 9:52 AM
Reply to this message Reply
Python, [x for x in aList if len(x.strip()) > 0]

Ruby, ["Paul", "", "George"].reject{ |name| name.empty? }

I really don't understand why the author seems to find it so difficult to go and read the Ruby API for arrays located at http://www.ruby-doc.org/core/classes/Array.html#M002219

This is what makes Ruby such a beautiful language: "Gee, I want to get rid of the few elements in an array, I wonder if this aptly named 'reject' function is what I need?" With Ruby, the answer is always YES!

Look at the two examples above. The Python version may be a tiny bit shorter, but it is completely opaque. There are x's everywhere and it's not at all clear what you are trying to accomplish. If you look at the Ruby version, you don't even have to know how to program to immediately see that you have a list of names and are trying to reject the empty ones. It might as well be plain English.

Eric Larson

Posts: 1
Nickname: elarson
Registered: Oct, 2008

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 29, 2008 1:15 AM
Reply to this message Reply
While I see your point regarding having a specific function for rejecting some part of an list, it doesn't answer the question of how do I do abstract operations on list. The nice thing about Python in this case is that the abstract operation is defined as a language construct. I will never have to look for the "reject" method because the underlying concept of list comprehensions takes care of the abstraction for me.

It is this kind of powerful idiom that makes Python such a great language. Another excellent feature of list comprehensions is that they are traditionally faster than a similar for loop. What's more, there is the itertools module that also speeds up the most common operation seen in programming, looping.

With that said, you make a good point regarding list comprehensions being difficult to understand. Once they care clear, it is difficult to see how you got along without them. Of course understanding them is not always a trivial undertaking.

Andy Dent

Posts: 165
Nickname: andydent
Registered: Nov, 2005

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 29, 2008 2:09 AM
Reply to this message Reply
> Python, [x for x in aList if len(x.strip()) > 0]
>
> Ruby, ["Paul", "", "George"].reject{ |name| name.empty? }
>
> I really don't understand why the author seems to find it
> so difficult to go and read the Ruby API

I was thinking of *constructing* an array, this just being one particular example of the rule being used to construct it and so, no, I didn't notice "reject". That in itself is an interesting difference between the languages - in Python there's the orthogonal comprehension structure within which I use a flexible "if" statement. In Ruby, I have to be aware of different methods in Enumerable. They may be intuitive to read, if you know they are there but as a grammatical construct, they require memorizing or looking up.

I am a little puzzled why I missed "select" and, when I get back home, will have to check the SDK listed in the Pickaxe book from which I was working, not having internet access at the time of writing the samples. If Ruby had the **help** command built in to match the Python interpreter, maybe it would have been more obvious to me :-) The samples were written as I developed the code with the resources at hand. IMHO it's a weakness of Ruby compared to Python that I had to refer to an external source.

James Watson

Posts: 2024
Nickname: watson
Registered: Sep, 2005

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 29, 2008 6:36 AM
Reply to this message Reply
> If you look at the Ruby version, you
> don't even have to know how to program to immediately see
> that you have a list of names and are trying to reject the
> empty ones. It might as well be plain English.

As someone who is not familiar with Ruby, I can tell you that is not correct.

nes

Posts: 137
Nickname: nn
Registered: Jul, 2004

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 29, 2008 8:01 AM
Reply to this message Reply
> Ruby, ["Paul", "", "George"].reject{ |name| name.empty? }

That looks almost like this Python:

filter(lambda name:name.strip(),["Paul","","George"])

Carson Gross

Posts: 153
Nickname: cgross
Registered: Oct, 2006

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 30, 2008 8:49 AM
Reply to this message Reply
> While I see your point regarding having a specific
> function for rejecting some part of an list, it doesn't
> answer the question of how do I do abstract operations on
> list.

I think it does answer that question. By having methods that take closures, rather than adding additional language syntax, it's very easy to answer the question "how do I do abstract operations on a list?" You go to the documentation on the List class and look at the methods it supports, just like you would with any other object. Those are your operations.

No new syntax to learn and, if you have a statically typed language or the mythical really-good-IDE-for-a-dynamically-typed-languages, you can see all those operations by simply hitting the '.' character after your list.

That's about as discoverable as it gets.

That being said, I'm a big fan of LINQ, which is sort of like list comprehensions but on steriods with missile explosions, flame-spewing v8's and kegs of beer. The Perfect Language (tm) would support both. And run on the JVM. (Sorry C#)

Cheers,
Carson

James Watson

Posts: 2024
Nickname: watson
Registered: Sep, 2005

Re: The Adventures of a Pythonista in Rubyland/2 - No Comprehension Posted: Oct 30, 2008 1:18 PM
Reply to this message Reply
> > While I see your point regarding having a specific
> > function for rejecting some part of an list, it doesn't
> > answer the question of how do I do abstract operations
> on
> > list.
>
> I think it does answer that question. By having methods
> that take closures, rather than adding additional language
> syntax, it's very easy to answer the question "how do I do
> abstract operations on a list?" You go to the
> documentation on the List class and look at the methods it
> supports, just like you would with any other object.
> Those are your operations.

I think the question is how you do something like this:

[x.lower() for x in aList if len(x.strip()) > 0]

using built in methods. It's not clear to me how you would do that in Ruby.

Flat View: This topic has 30 replies on 3 pages [ 1  2  3 | » ]
Topic: The decorator module version 3 is out! Previous Topic   Next Topic Topic: Ubuntu on the EeePC

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use