Interesting talk on Python vs. Ruby and how he would like Python to have just a bit more syntactic f


J

John Bokma

[..]
If it was Perl [1], I doubt it. Because line numbers are reported, and
if that doesn't help you, you can annotate anonymous functions with a
nick name using

local *__ANON__ = 'nice name'; [...]
As you can see, and a line number is generated, and the nice name is
shown.

Given that it has a nice name, what makes it an anonymous function?

You can't do

nice name();

It just changes what perl reports.
If this is the case, then your answer to "anonymous functions are a
PITA"

I don't think anon functions are in general a
PITA. Like with most things, (I) use them in moderation.
is "don't use anonymous functions", which exactly the same answer we'd
give here in Python land. The only difference is that Perl provides two
ways of making a named function, and Python only one[1].

Note that the local trick doesn't create a named function. There are
other ways of course to create named functions in Perl, e.g.

perl -e '*foo=sub { print "hello, world\n" }; foo();'

Which can be fun:

perl -e '
sub AUTOLOAD {
my $name = our $AUTOLOAD;
*$AUTOLOAD = sub { local $" = ", "; print "$name(@_)\n" };
goto &$AUTOLOAD;
}
foo(40);
bar("hello", "world!");
baz(foo(10));'

output:

main::foo(40)
main::bar(hello, world!)
main::foo(10)
main::baz(1)

NB: calling foo 10 returns 1 (return value of print).
 
Ad

Advertisements

S

Steve Howell

The name is used in Pascal, which probably means it originated from
Fortran or Algol.

A subroutine is a generic piece of code which can be re-used by some
unspecified mechanism (GOSUB in Basic, by calling it in most other
languages). A function is a subroutine that returns a result, and a
procedure is a subroutine that doesn't return anything (not even None, or
the equivalent thereof) and operates entirely by side-effect.

Those are useful clarifications, but they are not completely
universal.

Some people make the definition of function more restrictive--"if it
has side effects, it is not a function."

Python's definition of a method is also not universal. In some
circles, method is more akin to Steven's definition of a procedure--it
does not necessarily have to be associated with a class.

It's all very confusing, which is why Pythonistas are typically
adamant about clarifying definitions within Python's context, which is
understandable. To the extent that we're all talking about one
programming language, we should use the same terms.

A quick Google search does not turn up an official definition of a
Ruby block, although the term "block" is colloquially used in both
Python and Ruby to refer to a bunch of lines of code executed in a
particular context, like a loop.

Python may not support the broadest notion of anonymous functions, but
it definitely has anonymous blocks. You can write this in Python:

for i in range(10):
print i
print i * i
print i * i * i

Python does not force you to do this:

def do_stuff(i):
print i
print i * i
print i * i * i
for i in range(10):
do_stuff(i)
 
S

Steve Howell

I used to think anonymous functions (AKA blocks, etc...) would be a
nice feature for Python.

Then I looked at a stack trace from a different programming language
with lots of anonymous functions. (I believe it was perl.)

I became enlightened.

I use Ruby a lot in my day job, and we rarely use blocks are as
anonymous callback functions, which was probably the source of your
pain in other languages.

Often Ruby blocks are just three or four lines of code that are
inlined into a still small function, so as long as the outer function
is still small (which Ruby's blocks help with--they promote
terseness), it's pretty easy to find a buggy function within a
traceback that is not overly big.

It's also possible in Ruby to use quality-promoting techniques like
unit testing, pair programming, deliberateness, etc., to avoid the
need for looking at tracebacks in the first place.

Python is not immune to hard-to-understand tracebacks, since you often
don't often know how a method got itself into the stracktrace in the
first place:

Traceback (most recent call last):
File "foo.py", line 11, in <module>
foo(method)
File "foo.py", line 2, in foo
method()
File "foo.py", line 5, in bar
raise Exception('I am broken!')
Exception: I am broken!

Even though there's no direct lexical reference to bar() in foo(), lo
and behold, foo() ends up calling bar():

def foo(method):
method()

def bar():
raise Exception('I am broken!')

def broken_function_factory():
return bar

method = broken_function_factory()
foo(method)
 
C

Carl Banks

Jonathan Gardner said:
Then I looked at a stack trace from a different programming language
with lots of anonymous functions. (I believe it was perl.)
I became enlightened.

If it was Perl [1], I doubt it. Because line numbers are reported

Ok so sonetimes I'm looking at a stack trace and sometimes I can tell
what the bug is just by lookong at the function namess. But if it has
just line numbers I have to dig through my code looking for the line.

and
if that doesn't help you, you can annotate anonymous functions with a
nick name using

local *__ANON__ = 'nice name';

Finding an issue, and not looking for a solution is not called becoming
enlightened ;-)

The issue is that a stacktrace showing a bunch of nameless line
numbers can be a bad idea, not that Perl might be deficient (and we
already know that in any case), so it's not good to use a lot of
them. Anyway once you annotate an anonymous function, it's no longer
anonymous.

~$ perl -e '
use Carp;

my $anon = sub { local *__ANON__ = "hello, world"; croak "oops"; };
$anon->();
'

oops at -e line 4
        main::hello, world() called at -e line 5

As you can see, and a line number is generated, and the nice name is
shown.

If you generate anonymouse functions on the fly based on parameters, you
can encode this into the nice name, of course.

Sadly, often bold statements about a language are made in ignorance.

I don't see what he said was any kind of a bold statement about a
language, arguably it was about the coding style. That Perl allows
annotating functions doesn't mean people do it.


Carl Banks
[1] perl is the program that executes Perl programs ;-).
 
D

Dennis Lee Bieber

The name is used in Pascal, which probably means it originated from
Fortran or Algol.
Hopefully (counting 4 years of college) my 25+ years of FORTRAN
experience permits me to supply trivia...

FORTRAN had SUBPROGRAMS...

Subprograms were subdivided into:
Subroutine subprograms
and
Function subprograms

The former had to be explicitly "called", and any results were
returned via side-effect only.

call subroutine(arg, list)

The latter acted as "functions" in that they could be in-lined in
expressions or be the RHS of an assignment

result = function(arg, list)

but since all parameters are passed by reference in FORTRAN, side
effects are still possible (hence the real meaning of the term "side
effect" -- a change that occurs outside the bounds of the function
returning a value)
 
S

Steve Howell

Some problems with using just line numbers to track errors:

In any language it isn't much use if you get a bug report from a shipped
program that says there was an error on line 793 but no report of
exactly which version of the shipped code was being run.

Microsoft love telling you the line number: if IE gets a Javascript
error it reports line number but not filename, so you have to guess
which of the HTML page or one of many included files actually had the
error. Plus the line number that is reported is often slightly off.

Javascript in particular is often sent to the browser compressed then
uncompressed and eval'd. That makes line numbers completely useless for
tracking down bugs as you'll always get the line number of the eval.
Also the way functions are defined in Javascript means you'll often have
almost every function listed in a backtrace as 'Anonymous'.

If this is an argument against using anonymous functions, then it is a
quadruple strawman.

Shipping buggy code is a bad idea, even with named functions.

Obscuring line numbers is a bad idea, even with named functions.

Having your customers stay on older versions of your software is a bad
idea, even with named functions.

Not being able to know which version of software you're customer is
running is a bad idea, even with named functions.

Of course, using anonymous functions in no way prevents you from
capturing a version number in a traceback. And in most modern source
control systems, it is fairly easy to revert to an old version of that
code.

def factory():
return lambda: 15 / 0

def bar(method):
method()

def foo(method):
bar(method)

def baz(method):
foo(method)

try:
baz(factory())
except:
print 'problem with version 1.234a'
raise

problem with version 1.234a
Traceback (most recent call last):
File "foo.py", line 14, in <module>
baz(factory())
File "foo.py", line 11, in baz
foo(method)
File "foo.py", line 8, in foo
bar(method)
File "foo.py", line 5, in bar
method()
File "foo.py", line 2, in <lambda>
return lambda: 15 / 0
ZeroDivisionError: integer division or modulo by zero
 
Ad

Advertisements

S

Steve Howell

I doubt very much whether I have ever shipped any bug-free code but
even if it was fit for purpose when shipped it is quite possible that the
software will interact badly with other software that did not exist at the
time of shipping.




In principle I agree, but where Javascript is concerned compressing the
downloaded files is generally a pretty good idea and practicality beats
purity.




I think that's their decision, not mine.



mpr
I agree, but getting a complete coherent description out of a customer is
not always an easy task. (I'm reading the word 'customer' here to include
the case where there is no monetary relationship between the software
author and the entity using it, but even when there is I think this still
true.)

Just to be clear, I'm not saying it's unforgivable to occasionally
ship software with bugs. It happens.

Compressing Javascript is sometimes necessary, but I believe that
often mangles named functions too.

To the the extent that your customer is running old software and
cannot always coherently describe tracebacks over a telephone, that
problem can be solved in the software itself, assuming an Internet
connection. The software can capture the traceback and report back to
a server with the version number.

So, much of the argument against anonymous functions presented so far
is really orthogonal to whether functions are named or not.

Circling back to the original topic, Ruby blocks, I think there is a
misconception about how blocks are often used in Ruby. Usually Ruby
blocks are inlined into a function and execute within that function.
Example:

def print_numbers()
[1, 2, 3, 4, 5, 6].map { |n|
[n * n, n * n * n]
}.reject { |square, cube|
square == 25 || cube == 64
}.map { |square, cube|
cube
}.each { |n|
puts n
raise 'problem here'
}
end

print_numbers()

The bug that I inserted into the "each" block gets reported in the
traceback:

foo.rb:10:in `print_numbers': problem here (RuntimeError)
from foo.rb:2:in `each'
from foo.rb:2:in `print_numbers'
from foo.rb:14

(I do prefer Python tracebacks BTW, but that is again orthogonal to
blocks vs. named functions.)

The blocks of code in the above Ruby code are somewhat analogous to
blocks of code in Python that happen within certain control
structures, such as "if," "while," "with," etc. The extra
expressiveness of Ruby comes from the fact that you can act on those
blocks with your own method. Of course, there is always a tradeoff
between expressiveness and simplicity. I know the person who gave the
talk about Ruby vs. Python, and trust me, nine times out of ten, he
prefers Python's simplicity to Ruby's expressiveness. But he likes
blocks.

I'm in the same boat. I use Python a lot, Ruby less so, but when I'm
in Ruby-land, I actually enjoy the expressiveness of blocks. They're
not particularly dangerous, and they allow you to express certain
sequential operations tersely and sequentially. The contrived code
below maps numbers to squares and cubes, then rejects a couple tuples,
then maps back to cubes, then prints each of the cubes.

def print_numbers()
[1, 2, 3, 4, 5, 6].map { |n|
[n * n, n * n * n]
}.reject { |square, cube|
square == 25 || cube == 64
}.map { |square, cube|
cube
}.each { |n|
puts n
}
end

IMHO there is no reason that I should have to name the content of each
of those four blocks of code, nor should I have to introduce the
"lambda" keyword.

I don't have a less contrived example handy, but the techniques above
apply often when you are filtering and massaging data.
 
S

Steven D'Aprano

If this is an argument against using anonymous functions, then it is a
quadruple strawman.

There really ought to be a special level of Hell for people who misuse
"strawman" to mean "a weak or invalid argument" instead of what it
actually means, which is a weak or invalid argument NOT HELD by your
opponent, which you (generic you) made up specifically for the sake of
shooting down.

If you actually read what Duncan says, he prefixes his response with:

"Some problems with using just line numbers to track errors".

Duncan's post is an argument against relying on line numbers as your
main, or only, source of information about the location of bugs in
Javascript.

In fact, this post is remarkable for the sheer number of actual strawman
arguments that you (Steve Howell) use:

Shipping buggy code is a bad idea, even with named functions.

Strawman #1: nobody said that shipping buggy code was a good idea, with
or without named functions. But shipping buggy code *happens*, no matter
how careful you are, so you need to expect bug reports back from users.

(And they will be *hard to find* bugs, because if they were easy to find
you would have found them in your own testing before shipping.)

Obscuring line numbers is a bad idea, even with named functions.

Strawman #2: nobody said that obscuring line numbers was a good idea. But
apparently compressing Javascript is valuable for other reasons, and
obscuring the line numbers is the side-effect of doing so.

And even knowing the line numbers is not necessarily useful, because many
bugs aren't due to the line that raises the stack trace. Just because you
know the line which failed doesn't mean you know how to fix the bug.

Having your customers stay on older versions of your software is a bad
idea, even with named functions.

Strawman #3: nobody said that staying on older versions is a good idea.
But sometimes it happens whether you like it or not.

(Although I'd like to point out that from the end user's perspective,
sometimes we don't want your stinkin' new version with all the anti-
features and pessimations and will stick to the old version for as long
as possible. If you don't like it, then think a bit harder before adding
anti-features like fragile, easily-corrupted databases which perform
really, really badly when your home directory is mounted over the
network. I'm talking to you, Firefox developers.)

And it doesn't really matter: you either end-of-life the old version, in
which case you don't need to do anything about the bug report except say
"upgrade", or you decide to continue support, in which case it doesn't
matter whether the bug is reported for an old version or the latest
version, you still need to fix it.

Not being able to know which version of software you're customer is
running is a bad idea, even with named functions.

Strawman #4.

See the pattern? When you attack a position the other guy hasn't taken,
that's a strawman. When you make a weak argument, it's just a weak
argument.
 
J

Jonathan Gardner

    def print_numbers()
        [1, 2, 3, 4, 5, 6].map { |n|
            [n * n, n * n * n]
        }.reject { |square, cube|
            square == 25 || cube == 64
        }.map { |square, cube|
            cube
        }.each { |n|
            puts n
        }
    end

If this style of programming were useful, we would all be writing Lisp
today. As it turned out, Lisp is incredibly difficult to read and
understand, even for experienced Lispers. I am pleased that Python is
not following Lisp in that regard.

for n in range(1,6):
square = n*n
cube = n*n*n
if square == 25 or cube == 64: continue
print cube
 
S

Steven D'Aprano

Just to be clear, I'm not saying it's unforgivable to occasionally ship
software with bugs. It happens.

"Occasionally"? Oh, if only.

I would say that there probably isn't a non-trivial application in the
world that is entirely bug-free. If you're shipping something more
complex than the proverbial "Hello World", chances are high that there
will be bugs, and the more complex the app, the more bugs are likely.

Compressing Javascript is sometimes necessary, but I believe that often
mangles named functions too.

It doesn't mangle the function, it mangles reporting of line numbers. But
if you know the name of the function, it is much easier to recover from
that loss of information.

To the the extent that your customer is running old software and cannot
always coherently describe tracebacks over a telephone, that problem can
be solved in the software itself, assuming an Internet connection. The
software can capture the traceback and report back to a server with the
version number.

I don't understand why you repeatedly mention "old software". It is
irrelevant: the software is either supported, or not supported. If it's
not supported, you don't care about the bugs. If it is supported, then it
doesn't matter whether it is version 2.2 or 2.3 or the bleeding edge 2.4-
pre-alpha straight out of subversion, you still have to go through the
same process of finding the bug, solving it, then rolling the fix out to
all supported versions where the bug applies.

That's not to say that the version number isn't useful information to
have, because it can be, but distinguishing between old versions and the
current version isn't a useful distinction. In a sense, there are no old
versions, there are merely multiple supported current versions.
So, much of the argument against anonymous functions presented so far is
really orthogonal to whether functions are named or not.

Not so. The point is that anonymous functions lack useful information,
namely the function name. Because line numbers can be unreliable or even
missing completely, and even when reliable many people have a mental
blind-spot for them (I know I do, and I'm gratified to see I'm not the
only one), lacking a good name for the function is a handicap. Not
necessarily an insurmountable one, but anonymous functions are more
troublesome than named functions.

You wouldn't name your functions:

f01, f02, f03, f04, ... f99

(say), unless you were trying to deliberately obfuscate your code.
Anonymous functions are even more obfuscated than that. You can get away
with it so long as you're only dealing with a few, in well-defined
placed, but you wouldn't want to use them all over the place.
 
S

sjdevnull

    def print_numbers()
        [1, 2, 3, 4, 5, 6].map { |n|
            [n * n, n * n * n]
        }.reject { |square, cube|
            square == 25 || cube == 64
        }.map { |square, cube|
            cube
        }.each { |n|
            puts n
        }
    end

IMHO there is no reason that I should have to name the content of each
of those four blocks of code, nor should I have to introduce the
"lambda" keyword.

You could do it without intermediate names or lambdas in Python as:
def print_numbers():
for i in [ cube for (square, cube) in
[(n*n, n*n*n) for n in [1,2,3,4,5,6]]
if square!=25 and cube!=64 ]:
print i

But frankly, although there's no reason that you _have_ to name the
content at each step, I find it a lot more readable if you do:

def print_numbers():
tuples = [(n*n, n*n*n) for n in (1,2,3,4,5,6)]
filtered = [ cube for (square, cube) in tuples if square!=25 and
cube!=64 ]
for f in filtered:
print f
 
Ad

Advertisements

J

John Bokma

Jonathan Gardner said:
    def print_numbers()
        [1, 2, 3, 4, 5, 6].map { |n|
            [n * n, n * n * n]
        }.reject { |square, cube|
            square == 25 || cube == 64
        }.map { |square, cube|
            cube
        }.each { |n|
            puts n
        }
    end

If this style of programming were useful, we would all be writing Lisp
today. As it turned out, Lisp is incredibly difficult to read and
understand, even for experienced Lispers. I am pleased that Python is
not following Lisp in that regard.

for n in range(1,6):

^ should be 7

But for the rest, I agree with you. I can read Steve's version, but even
to an experienced Perl programmer that looks quite noisy :)
 
J

John Bokma

John Bokma said:
Jonathan Gardner said:
    def print_numbers()
        [1, 2, 3, 4, 5, 6].map { |n|
            [n * n, n * n * n]
        }.reject { |square, cube|
            square == 25 || cube == 64
        }.map { |square, cube|
            cube
        }.each { |n|
            puts n
        }
    end

If this style of programming were useful, we would all be writing Lisp
today. As it turned out, Lisp is incredibly difficult to read and
understand, even for experienced Lispers. I am pleased that Python is
not following Lisp in that regard.

for n in range(1,6):

^ should be 7

But for the rest, I agree with you. I can read Steve's version, but even
to an experienced Perl programmer that looks quite noisy :)

Oh, wait, it's Ruby :-D.
 
J

Jonathan Gardner

You could do it without intermediate names or lambdas in Python as:
def print_numbers():
    for i in [ cube for (square, cube) in
                         [(n*n, n*n*n) for n in [1,2,3,4,5,6]]
               if square!=25 and cube!=64 ]:
        print i

But frankly, although there's no reason that you _have_ to name the
content at each step, I find it a lot more readable if you do:

def print_numbers():
    tuples = [(n*n, n*n*n) for n in (1,2,3,4,5,6)]
    filtered = [ cube for (square, cube) in tuples if square!=25 and
cube!=64 ]
    for f in filtered:
        print f

Step away from the keyboard! This is a programmer's arrest!

There are laws around here, laws that we can't allow to be broken.
You've just broken 12 of them. You think the laws don't apply to you,
huh, punk? HUH?

I'm sentencing you to three months HARD LABOR in Ruby for that code
you just wrote. And if you think it's too harsh, then I'll sentence
you to NINE MONTHS PHP and see how you feel about that!

;-)
 
S

Steve Howell

[...]
You wouldn't name your functions:

f01, f02, f03, f04, ... f99

Exactly.

(say), unless you were trying to deliberately obfuscate your code.
Anonymous functions are even more obfuscated than that. You can get away
with it so long as you're only dealing with a few, in well-defined
placed, but you wouldn't want to use them all over the place.

I have contributed to the confusion of this discussion by talking
about "anonymous functions," when the original context was "anonymous
blocks." As I mentioned in an earlier response, most anonymous blocks
in Ruby are placed within outer functions, so they're not that hard to
locate in a traceback that provides only function names. And, of
course, it is often the case that you host Ruby code on your own web
server, or that you distribute Ruby code without compressing it, in
which case you get a sane traceback that provides line numbers.

You actually use anonymous blocks in your own code, in a few, well-
defined places (generally loops).

These excerpts are taken from obfuscate.py:

quotient = a//mm
a, mm = mm, a%mm
xx, x = x - quotient*xx, xx
yy, y = y - quotient*yy, yy


rail = it.next() # The rail we add to.
assert 0 <= rail < rails
fence[rail].append(c)


# Save one non-chaff character.
buffer.append(msg.next())
# And toss away more chaff.
n = self.hash(key) % factor
key = self.mod_key(key)
self.get_chars(n, msg)


# Careful here! Not all classes have a __dict__!
adict = getattr(obj, '__dict__', {})
for name, attr in adict.items():
if inspect.ismethoddescriptor(attr):
d[nm + '.' + name] = attr.__get__(obj)

If any of the above code were to fail on a customer site, you'd
probably want to get line numbers in a traceback. I'm guessing you
probably don't distribute your code in compressed form, and you
probably take care to make sure it works right in the first place, and
you probably have source control to help you pull up old versions of
your code. I notice that you even have a __version__ identifier in
your source code, which users of your library could capture in their
tracebacks. In other words, you probably use mostly the same
practices that I use, except that we seem to differ on the utility or
expressiveness or Ruby blocks, or maybe we're arguing at cross
purposes.
 
S

Steve Holden

Steven said:
On Thu, 18 Feb 2010 06:15:20 -0800, Steve Howell wrote: [...]
There really ought to be a special level of Hell for people who misuse
"strawman" to mean "a weak or invalid argument" instead of what it
actually means, which is a weak or invalid argument NOT HELD by your
opponent, which you (generic you) made up specifically for the sake of
shooting down.

If you actually read what Duncan says, he prefixes his response with:

"Some problems with using just line numbers to track errors".

Duncan's post is an argument against relying on line numbers as your
main, or only, source of information about the location of bugs in
Javascript.

In fact, this post is remarkable for the sheer number of actual strawman
arguments that you (Steve Howell) use:

Shipping buggy code is a bad idea, even with named functions.

Strawman #1: nobody said that shipping buggy code was a good idea, with
or without named functions. But shipping buggy code *happens*, no matter
how careful you are, so you need to expect bug reports back from users.

(And they will be *hard to find* bugs, because if they were easy to find
you would have found them in your own testing before shipping.)

Obscuring line numbers is a bad idea, even with named functions.

Strawman #2: nobody said that obscuring line numbers was a good idea. But
apparently compressing Javascript is valuable for other reasons, and
obscuring the line numbers is the side-effect of doing so.

And even knowing the line numbers is not necessarily useful, because many
bugs aren't due to the line that raises the stack trace. Just because you
know the line which failed doesn't mean you know how to fix the bug.

Having your customers stay on older versions of your software is a bad
idea, even with named functions.

Strawman #3: nobody said that staying on older versions is a good idea.
But sometimes it happens whether you like it or not.

(Although I'd like to point out that from the end user's perspective,
sometimes we don't want your stinkin' new version with all the anti-
features and pessimations and will stick to the old version for as long
as possible. If you don't like it, then think a bit harder before adding
anti-features like fragile, easily-corrupted databases which perform
really, really badly when your home directory is mounted over the
network. I'm talking to you, Firefox developers.)

And it doesn't really matter: you either end-of-life the old version, in
which case you don't need to do anything about the bug report except say
"upgrade", or you decide to continue support, in which case it doesn't
matter whether the bug is reported for an old version or the latest
version, you still need to fix it.

Not being able to know which version of software you're customer is
running is a bad idea, even with named functions.

Strawman #4.

See the pattern? When you attack a position the other guy hasn't taken,
that's a strawman. When you make a weak argument, it's just a weak
argument.
Next week: Lesson 2 - Ad Hominem Attacks

regards
Steve
 
Ad

Advertisements

S

Steve Howell

    def print_numbers()
        [1, 2, 3, 4, 5, 6].map { |n|
            [n * n, n * n * n]
        }.reject { |square, cube|
            square == 25 || cube == 64
        }.map { |square, cube|
            cube
        }.each { |n|
            puts n
        }
    end
IMHO there is no reason that I should have to name the content of each
of those four blocks of code, nor should I have to introduce the
"lambda" keyword.

You could do it without intermediate names or lambdas in Python as:
def print_numbers():
    for i in [ cube for (square, cube) in
                         [(n*n, n*n*n) for n in [1,2,3,4,5,6]]
               if square!=25 and cube!=64 ]:
        print i


The problem with list comprehensions is that they read kind of out of
order. On line 2 you are doing the first operation, then on line 3
you are filtering, then on line 1 your are selecting, then on line 4
you are printing.

For such a small example, your code is still quite readable.
But frankly, although there's no reason that you _have_ to name the
content at each step, I find it a lot more readable if you do:

def print_numbers():
    tuples = [(n*n, n*n*n) for n in (1,2,3,4,5,6)]
    filtered = [ cube for (square, cube) in tuples if square!=25 and
cube!=64 ]
    for f in filtered:
        print f

The names you give to the intermediate results here are
terse--"tuples" and "filtered"--so your code reads nicely.

In a more real world example, the intermediate results would be
something like this:

departments
departments_in_new_york
departments_in_new_york_not_on_bonus_cycle
employees_in_departments_in_new_york_not_on_bonus_cycle
names_of_employee_in_departments_in_new_york_not_on_bonus_cycle
 
P

Paul Rubin

Steve Howell said:
But frankly, although there's no reason that you _have_ to name the
content at each step, I find it a lot more readable if you do:

def print_numbers():
    tuples = [(n*n, n*n*n) for n in (1,2,3,4,5,6)]
    filtered = [ cube for (square, cube) in tuples if square!=25 and
cube!=64 ]
    for f in filtered:
        print f

The names you give to the intermediate results here are
terse--"tuples" and "filtered"--so your code reads nicely.

But that example makes tuples and filtered into completely expanded
lists in memory. I don't know Ruby so I've been wondering whether the
Ruby code would run as an iterator pipeline that uses constant memory.
In a more real world example, the intermediate results would be
something like this:

departments
departments_in_new_york
departments_in_new_york_not_on_bonus_cycle
employees_in_departments_in_new_york_not_on_bonus_cycle
names_of_employee_in_departments_in_new_york_not_on_bonus_cycle

http://haskell.org/ghc/docs/6.10.4/...ax-extns.html#generalised-list-comprehensions

might be of interest. Maybe Ruby and/or Python could grow something similar.
 
S

sjdevnull

Steve Howell said:
But frankly, although there's no reason that you _have_ to name the
content at each step, I find it a lot more readable if you do:
def print_numbers():
    tuples = [(n*n, n*n*n) for n in (1,2,3,4,5,6)]
    filtered = [ cube for (square, cube) in tuples if square!=25 and
cube!=64 ]
    for f in filtered:
        print f
The names you give to the intermediate results here are
terse--"tuples" and "filtered"--so your code reads nicely.

But that example makes tuples and filtered into completely expanded
lists in memory.  I don't know Ruby so I've been wondering whether the
Ruby code would run as an iterator pipeline that uses constant memory.

I don't know how Ruby works, either. If it's using constant memory,
switching the Python to generator comprehensions (and getting constant
memory usage) is simply a matter of turning square brackets into
parentheses:

def print_numbers():
tuples = ((n*n, n*n*n) for n in (1,2,3,4,5,6))
filtered = ( cube for (square, cube) in tuples if square!=25 and
cube!=64 )
for f in filtered:
print f

Replace (1,2,3,4,5,6) with xrange(100000000) and memory usage still
stays constant.

Though for this particular example, I prefer a strict looping solution
akin to what Jonathan Gardner had upthread:

for n in (1,2,3,4,5,6):
square = n*n
cube = n*n*n
if square == 25 or cube == 64: continue
print cube

I don't think the assertion that the names would be ridiculously long
is accurate, either.

Something like:

departments = blah
ny_depts = blah(departments)
non_bonus_depts = blah(ny_depts)
non_bonus_employees = blah(non_bonus_depts)
employee_names = blah(non_bonus_employees)

If the code is at all well-structured, it'll be just as obvious from
the context that each list/generator/whatever is building from the
previous one as it is in the anonymous block case.
 
Ad

Advertisements

S

Steve Howell

    def print_numbers()
        [1, 2, 3, 4, 5, 6].map { |n|
            [n * n, n * n * n]
        }.reject { |square, cube|
            square == 25 || cube == 64
        }.map { |square, cube|
            cube
        }.each { |n|
            puts n
        }
    end

If this style of programming were useful, we would all be writing Lisp
today. As it turned out, Lisp is incredibly difficult to read and
understand, even for experienced Lispers. I am pleased that Python is
not following Lisp in that regard.

for n in range(1,6):
    square = n*n
    cube = n*n*n
    if square == 25 or cube == 64: continue
    print cube

There's definitely a cognitive dissonance between imperative
programming and functional programming. It's hard for programmers
used to programming in an imperative style to appreciate a functional
approach, because functional solutions often read "upside down" in the
actual source code and common algebraic notation:

def compute_squares_and_cubes(lst):
return [(n * n, n * n * n) for n in lst]

def reject_bad_values(lst):
return [(square, cube) for (square, cube) \
in lst if not (square == 25 or cube == 64)]

def cubes_only(lst):
return [cube for square, cube in lst]

def print_results(lst):
# 1. compute_squares_and_cubes
# 2. reject_bad_values
# 3. take cubes_only
# 4. print values
for item in \
cubes_only( # 3
reject_bad_values( # 2
compute_squares_and_cubes(lst))): # 1
print item # 4

You can, of course, restore the natural order of operations to read
top-down with appropriate use of intermediate locals:

def print_results(lst):
lst2 = compute_squares_and_cubes(lst)
lst3 = reject_bad_values(lst2)
lst4 = cubes_only(lst3)
for item in lst4:
print item
 

Top