Yielding an object and caring about the result: the cousin of Object#tap

F

furtive.clown

The idea of Object#tap is to insert a "listener" (like tapping a phone
line) into the object chain without affecting the object (without
being caught eavesdropping).

On the other hand, we often wish to transform the current object. I
use Object#as like this: take the current object, name it *as*
something, do something with it, and give the result.

class Object
def as
yield self
end
end

Compare:

all_files2 = transform2(source_files + transform1(
stems.map { |f|
f + "." + guess_extension(f)
})).map { |f|
File.join(dir, f)
}

with:

all_files = stems.map { |f|
f + "." + guess_extension(f)
}.as { |t|
transform2(source_files + transform1(t))
}.map { |f|
File.join(dir, f)
}

The former is so disheveled that I don't even know how to indent it.
On the other hand, the latter is beautiful (to me).

Incidentally I'm all for renaming Object#as to whatever you wish. I
chose it because it expresses the intent: name the object *as*
something.

--FC
 
F

furtive.clown

I forgot to include this option:

t = stems.map { |f|
f + "." + guess_extension(f)
}
all_files3 = transform2(source_files + transform1(t)).map { |f|
File.join(dir, f)
}

Introducing needless temporaries on the same scope level as the target
variable makes understanding and maintenance harder. Look again:

all_files = stems.map { |f|
f + "." + guess_extension(f)
}.as { |t|
transform2(source_files + transform1(t))
}.map { |f|
File.join(dir, f)
}

This clearly communicates that the purpose of the block chain is to
obtain a value for all_files. From beginning to end, we understand
the intent at a glance. In the former case, the intent is muddled by
a useless variable floating around. Multiply this example by 10 and
it quickly becomes the difference between beauty and travesty.

This was my previous response (with editing) to the suggestion that I
should use Object#instance_eval instead of Object#as:

What if I want to use the "self" before the instance_eval? What if I
use "self" inside the block while forgetting that I'm inside an
instance_eval? I'd be screwed, and screwing would serve no purpose
except to avoid defining Object#as.

The use instance_eval communicates a specific purpose which is
entirely different from the purpose of Object#as. I want to take the
current object, name it *as* something, perform some operations on it,
and give the result.

The current object should not be given the name "self", which is a
special name. It should be given a temporary name (e.g. "t") which
communicates its temporal non-specialness. Object#instance_eval is
the former, Object#as is the latter.
 
R

Robert Klemme

2007/11/13 said:
I forgot to include this option:

t = stems.map { |f|
f + "." + guess_extension(f)
}
all_files3 = transform2(source_files + transform1(t)).map { |f|
File.join(dir, f)
}

Introducing needless temporaries on the same scope level as the target
variable makes understanding and maintenance harder. Look again:

IMHO introducing temporary variables with proper names can go a long
way to make this piece much more readable. (The same holds true for
method names.)
all_files = stems.map { |f|
f + "." + guess_extension(f)
}.as { |t|
transform2(source_files + transform1(t))
}.map { |f|
File.join(dir, f)
}

This clearly communicates that the purpose of the block chain is to
obtain a value for all_files. From beginning to end, we understand
the intent at a glance.

Frankly, I don't.
In the former case, the intent is muddled by
a useless variable floating around. Multiply this example by 10 and
it quickly becomes the difference between beauty and travesty.

Obviously preferences differ. Although I can only guess what all
those methods do, I would prefer this variant:

all_inputs = source_files +
transform1( stems.map { |f| f + "." + guess_extension(f) } )
all_files = transform2( all_inputs ).map {|f| File.join dir, f}

Note, if transform2 and transform1 do not need the whole collection I
would change their implementation to transform a single value only
which then would make the whole story a lot simpler. The current
approach seems quite complex to me but without knowing what all this
is supposed to do I have no better variant to offer.

Kind regards

robert
 
F

furtive.clown

IMHO introducing temporary variables with proper names can go a long
way to make this piece much more readable. (The same holds true for
method names.)

OK, point taken about the proper name. Nonetheless I am surprised
this is not a unanimous slam dunk. The contenders are:

all_inputs = source_files +
transform1( stems.map { |f| f + "." + guess_extension(f) } )
all_files = transform2( all_inputs ).map {|f| File.join dir, f}

verses

all_files = stems.map { |f|
f + "." + guess_extension(f)
}.as { |all_inputs|
transform2(source_files + transform1(all_inputs))
}.map { |f|
File.join dir, f
}

Since I have been accustomed to using #as for years, the latter looks
canonical and the former looks quirky and too clever. Three things
come to mind:

(1) Parentheses adjacent to brackets are an eyesore.

(2) The latter is step-by-step linear and easier to follow: one
transformation, then another, then another. Simple. The former takes
much more studying in order to understand, relatively speaking. That
is, my strategy for understanding it is to work from the inmost
expression outward. But in the latter case, I work linearly from
start to finish.

(3) What is the purpose of all_inputs? Is it there as an
intermediate, or do you want to use it for something else? Should I
keep a mental note of it, or should I discard it in my mind? In the
former case, I am left wondering all of this. In the latter case the
answer is clear: it's a temporary for building all_files, to be
forgotten as soon as possible.

I am a lone rubyist who went off the grid several years ago after
contributing a project on rubyforge, so perhaps *my* style is the
quirky one. Still, in my mind the latter is sheer elegance compared
to the former. I actually find it interesting that someone could
possibly disagree. Do (1)-(3) make any sense, then?

Regards,
-FC
 
R

Robert Klemme

2007/11/13 said:
OK, point taken about the proper name. Nonetheless I am surprised
this is not a unanimous slam dunk. The contenders are:

all_inputs = source_files +
transform1( stems.map { |f| f + "." + guess_extension(f) } )
all_files = transform2( all_inputs ).map {|f| File.join dir, f}

verses

all_files = stems.map { |f|
f + "." + guess_extension(f)
}.as { |all_inputs|
transform2(source_files + transform1(all_inputs))
}.map { |f|
File.join dir, f
}

Since I have been accustomed to using #as for years, the latter looks
canonical and the former looks quirky and too clever. Three things
come to mind:

(1) Parentheses adjacent to brackets are an eyesore.

I don't subscribe to that. It depends on the individual case.
(2) The latter is step-by-step linear and easier to follow: one
transformation, then another, then another. Simple. The former takes
much more studying in order to understand, relatively speaking. That
is, my strategy for understanding it is to work from the inmost
expression outward. But in the latter case, I work linearly from
start to finish.

As I said, preferences differ. Since you are used to working this way
it's easy for you. For me it took quite a bit of time until I
understood the logic.
(3) What is the purpose of all_inputs? Is it there as an
intermediate, or do you want to use it for something else? Should I
keep a mental note of it, or should I discard it in my mind? In the
former case, I am left wondering all of this. In the latter case the
answer is clear: it's a temporary for building all_files, to be
forgotten as soon as possible.

It never occurred to me that this could be a problem. If you have too
many temporary variables in a method the method is probably too long
or complex anyway.
I am a lone rubyist who went off the grid several years ago after
contributing a project on rubyforge, so perhaps *my* style is the
quirky one. Still, in my mind the latter is sheer elegance compared
to the former. I actually find it interesting that someone could
possibly disagree. Do (1)-(3) make any sense, then?

Partly but I'd rather hear other voices as well.

Btw, what is all this transforming doing? Can you give a short
description of what this piece of code is supposed to do? As far as I
can see you have inputs source_files and stems and get a single list
of filenames out of this. Can you give some more hints about the
semantics?

Kind regards

robert
 
D

Daniel DeLorme

I am a lone rubyist who went off the grid several years ago after
contributing a project on rubyforge, so perhaps *my* style is the
quirky one. Still, in my mind the latter is sheer elegance compared
to the former. I actually find it interesting that someone could
possibly disagree. Do (1)-(3) make any sense, then?

This kind of suggestion has been made several times in the past and,
while I agree it looks elegant, I believe the issue is that "as" does
not really /belong/ to the object on which it is called. It is a utility
method. If you look at the base methods of Object you'll see that all of
them have to do with the state of the object itself (except for the ugly
exception of "display"). Semantically, a utility method has no business
being part of the public interface of every object. A better solution
would be with(expr){ |var| ... but that kills the elegance doesn't it? I
sympathize, I really do. I have the same issue with my pet method "ergo"

Lately I've been thinking that this could tie into Ara's concept of
"pervasive" methods. If we define a pervasive method as a method that
applies to all objects, then we could do something like
Pervasives.define do
def as
yield(self)
end
end
Which could be called reliably as:
Pervasives.call(expr, :as){ |obj| ... }
or less reliably through method_missing:
expr.as{ |obj| ... }
leaving the Object methodspace unpolluted:
expr.methods.include?("as") #=> false

no?

Daniel
 
R

Robert Klemme

2007/11/13 said:
This kind of suggestion has been made several times in the past and,
while I agree it looks elegant, I believe the issue is that "as" does
not really /belong/ to the object on which it is called. It is a utility
method. If you look at the base methods of Object you'll see that all of
them have to do with the state of the object itself (except for the ugly
exception of "display"). Semantically, a utility method has no business
being part of the public interface of every object. A better solution
would be with(expr){ |var| ... but that kills the elegance doesn't it? I
sympathize, I really do. I have the same issue with my pet method "ergo"

Lately I've been thinking that this could tie into Ara's concept of
"pervasive" methods. If we define a pervasive method as a method that
applies to all objects, then we could do something like
Pervasives.define do
def as
yield(self)
end
end
Which could be called reliably as:
Pervasives.call(expr, :as){ |obj| ... }
or less reliably through method_missing:
expr.as{ |obj| ... }
leaving the Object methodspace unpolluted:
expr.methods.include?("as") #=> false

no?

Hmm... But this has the drawback that - since it's not defined on
Object - it's difficult to be aware of this method. Whether it's
defined explicitly or called implicitly via method_missing all objects
will properly respond to it. I am not sure I understand the benefit
of not explicitly defining it..

Cheers

robert
 
D

Daniel DeLorme

Robert said:
Hmm... But this has the drawback that - since it's not defined on
Object - it's difficult to be aware of this method. Whether it's

It doesn't need to be explicitly defined on Object if you know that it's
*pervasive* i.e. by definition available on all objects. Actually the
same could be extended to all /methods|instance_var/ methods. Don't you
ever do obj.methods and find yourself wishing all those were not there
to clutter the bigger picture of what the object *does*?
defined explicitly or called implicitly via method_missing all objects
will properly respond to it. I am not sure I understand the benefit
of not explicitly defining it..

Indeed the behavior would be pretty much the same. The biggest
difference is semantics I guess. Utility methods do not belong to the
public interface, and therefore calling them through the public
interface should be seen as mere synctatic sugar. A thin line to draw, I
agree. Also the distinction between pseudo-public utility methods and
true public methods may be a mere artifact of my mind, as 1.9 now
includes tap, to_enum, and enum_for, which *definitely* classify as
utility methods in my mind.

Daniel
 
R

Robert Klemme

2007/11/13 said:
It doesn't need to be explicitly defined on Object if you know that it's
*pervasive* i.e. by definition available on all objects. Actually the
same could be extended to all /methods|instance_var/ methods. Don't you
ever do obj.methods and find yourself wishing all those were not there
to clutter the bigger picture of what the object *does*?

Actually not, because I can do obj.public_methods(false). Also, if I
want to know what a particular method does I go to the documentation
anyway.
Indeed the behavior would be pretty much the same. The biggest
difference is semantics I guess. Utility methods do not belong to the
public interface, and therefore calling them through the public
interface should be seen as mere synctatic sugar. A thin line to draw, I
agree.

I had to wipe my glasses before I could spot it... :)
Also the distinction between pseudo-public utility methods and
true public methods may be a mere artifact of my mind, as 1.9 now
includes tap, to_enum, and enum_for, which *definitely* classify as
utility methods in my mind.

I have to say I like #to_enum very much but I agree that there is the
issue of cluttering namespaces. But IMHO no matter what you do (i.e.
explicit or implicit definition) the root problem does not go away
unless things like selector namespaces come into existence (i.e. a
particular view of a class / object in a context). As long as that
does not exist there is always potential for name clashes no matter
what.

Cheers

robert
 
F

furtive.clown

This kind of suggestion has been made several times in the past and,
while I agree it looks elegant, I believe the issue is that "as" does
not really /belong/ to the object on which it is called. It is a utility
method. If you look at the base methods of Object you'll see that all of
them have to do with the state of the object itself (except for the ugly
exception of "display"). Semantically, a utility method has no business
being part of the public interface of every object.

I think we are generally on the same page, but I would add that to_a,
to_ary, to_hash, to_enum, to_s are utilities as well since they just
output stuff. If these conversion utilities were not part of the
class then there would be a lot less chaining going on, and
consequently a lot fewer happy programmers. Ruby got that one right,
even though the fuddy-duddies object to the mutual tying between
classes for the mere sake of convenience.

Therefore it would be consistent to also desire Object#to_arg, another
conversion utility (a.k.a "as") which puts the thing into a block
argument, just as Hash#to_a puts the thing into an array. (The
analogy is a little forced but still convincing.) Ruby has already
committed itself to making happy programmers instead of happy language
theoreticians --- why not go this one step further? Somewhere out
there is a ruby programmer who, though he may not know it now, will be
made happier by it.

A second, separate argument is that the addition of Object#tap should
imply the addition of its complement, Object#as (or Object#to_arg, or
whatever we call it). A rubyist should notice something inconsistent
in being able to yield an object to a block without affecting the
object, but being unable to yield an object to a block in order to
produce a result.

--FC
 
A

ara.t.howard

I forgot to include this option:

t = stems.map { |f|
f + "." + guess_extension(f)
}
all_files3 = transform2(source_files + transform1(t)).map { |f|
File.join(dir, f)
}

Introducing needless temporaries on the same scope level as the target
variable makes understanding and maintenance harder. Look again:

all_files = stems.map { |f|
f + "." + guess_extension(f)
}.as { |t|
transform2(source_files + transform1(t))
}.map { |f|
File.join(dir, f)
}

the very real problem with this sort of thing is that exceptions will
map to one fugly line, making debugging for some poor soul a
nightmare. imagine that 'guess_extension' returned nil, or
'transform2' for that matter. this kind of golfing is great for
perl, but i've found over many painful experiences that it offers
little in the way of clarity or maintainability in production code.
i think at least a few ruby hackers, hardened by sunday night
debugging sessions, and anyone fleeing perl, will agree that
something along the lines of

guesses = stems.map{|stem| "#{ stem }.#{ guess_extension stem }"}
basenames = transform2 source_files + transform1(guesses)
expanded = basenames.map{|basename| File.join dir basename}

is clear, maintainable, and easy to understand even if you don't
dream of lambda calculus. come back six months later and add
transform3 and you'll appreciated what i'm talking about

and of course this isn't even addressing the issue that anyone
*really* naming a function transform1 or transform2 should be
smothered in their sleep, nor the fact that such functionality
should, in something as beautiful as ruby, end up looking like

filelist = Filelist.for source_files, :stems => stems, :dir => dir

making the pimpl nearly *irrelevant* so long as it's clear and allows
easy debugging and testing - testing being another *huge* checkmark
against monolithic method chains.
This clearly communicates that the purpose of the block chain is to
obtain a value for all_files. From beginning to end, we understand
the intent at a glance. In the former case, the intent is muddled by
a useless variable floating around. Multiply this example by 10 and
it quickly becomes the difference between beauty and travesty.

i strongly disagree: variable names are one of the single most
important elements of writing maintainable code - unless you happen
to be one of the very few who loves writing documentation (i
certainly don't). variables being references in ruby, i consider it
something almost evil *not* to through in a well named variable where
it lends clarity by literally spelling out the programmer's intent,
cuts line length, makes transitioning or modifying the code later
vastly easier, and stacktraces actually meaningful

none of this is to say that an Object#as or Object#tap is a bad idea
or that it always makes things less clear. but it's really a stretch
to say it makes the above logic clear. i used to think

for(i = j = k = 0; i < irank(matrix) && j < jrank(matrix) && krank
(matrix); i++, j++, k++){
...

was clever, and now it makes me want to run screaming into the woods
yelling 'internal iterator god damn it!'

so my concern with #as and #tap is that the lend strong support to
bad programming practices - making what should be hidden and internal
exposed and external.

kind regards.

a @ http://codeforpeople.com/
 
R

Ryan Davis

I agree with the whole of Ara's response and was debating whether to
respond to the OP at all... Ara obviates that desire. But, I did have
to respond to this:

and of course this isn't even addressing the issue that anyone
*really* naming a function transform1 or transform2 should be
smothered in their sleep,

OMG I simply love you for this Ara... I totally agree that we need
more smothering going on. ;)
nor the fact that such functionality should, in something as
beautiful as ruby, end up looking like

filelist = Filelist.for source_files, :stems => stems, :dir => dir

Exactly...

The real "need" for #as comes from a even more real need to clean up
one's design in the first place. Do that and #as simply drops away.
 
D

Daniel Waite

ara.t.howard said:
the very real problem with this sort of thing is that exceptions will
map to one fugly line, making debugging for some poor soul a
nightmare. imagine that 'guess_extension' returned nil, or
'transform2' for that matter. this kind of golfing is great for
perl, but i've found over many painful experiences that it offers
little in the way of clarity or maintainability in production code.
i think at least a few ruby hackers, hardened by sunday night
debugging sessions, and anyone fleeing perl, will agree that
something along the lines of

guesses = stems.map{|stem| "#{ stem }.#{ guess_extension stem }"}
basenames = transform2 source_files + transform1(guesses)
expanded = basenames.map{|basename| File.join dir basename}

is clear, maintainable, and easy to understand even if you don't
dream of lambda calculus. come back six months later and add
transform3 and you'll appreciated what i'm talking about

and of course this isn't even addressing the issue that anyone
*really* naming a function transform1 or transform2 should be
smothered in their sleep, nor the fact that such functionality
should, in something as beautiful as ruby, end up looking like

filelist = Filelist.for source_files, :stems => stems, :dir => dir

*claps* Beautiful. Absolutely beautiful.

At first I dug what the OP was talking about with #as and also found it
a bit odd that it wasn't a "slam dunk" for others.

But Ara's examples explain _why_ there was no slam dunk. #as is cool,
but Ara's suggestions are simply good programming.

If I had to choose between any of the examples given (whether OP, JEII,
etc.) I'd go with what Ara has put forth.

Off-topic, are there any Smalltalkers here who feel there's a boundary
between object-oriented programming and functional programming? Can the
two (do the two) coexist?
 
J

James Edward Gray II

*claps* Beautiful. Absolutely beautiful.

Yeah, I hate to send in a "me too" post, but heck:

Me too!

Ara communicated everything I have been trying to say far better than
I did or even could have. I need to print his message out and put it
on the wall in my office.

James Edward Gray II
 
F

furtive.clown

Ara,

I have not had such problems with exception traces. Emacs parses the
trace, so I've never had a problem tracking exceptions down.

You said you disagreed about variable names, but I didn't make an
argument about variable names. The argument was about needless
temporaries lying around and mucking up at-a-glance comprehension.
Those temporaries can be culled away nicely with Object#as.

expanded = stems.map { |stem|
"#{ stem }.#{ guess_extension stem }"
}.as { |guesses|
transform2(source_files + transform1(guesses))
}.as { |basenames|
basenames.map { |basename|
File.join dir, basename
}
}

Of course transform1 and transform2 are bad names --- they are meant
to be bad! They are intentionally generic for the purpose of the
example. They stand for any function.

You gave the Filelist example as the preferred code, which is exactly
right. However we are not talking about Filelist, but the
*implementation* of Filelist.

Could you clarify how the above code lends "strong support to bad
programming practices"? That seems unfounded. On the contrary, I
find it more appealing at least because (1) the intent of the code ---
to produce a value for 'expanded' --- is more obvious; (2) there are
no temporaries lying around which may potentially cause confusion in
future maintenance; (3) the logic flow is more obvious: three
consecutive operations, with the output of one fed into the input of
the next.

Exceptionally Kind Regards,
-FC
 
D

Daniel Waite

James said:
Me too!

Ara communicated everything I have been trying to say far better than
I did or even could have. I need to print his message out and put it
on the wall in my office.

James Edward Gray II

No worries there, James. I, too would like to post this somewhere
(writing a blog entry right now), but after trying to quote him, the
text suddenly feels lacking the punch I felt when I first read it. I
think this is due, in part, to the lack of context.

Do you think we can extract a general principle from what he's said?
Maybe something akin to the "tips" in The Pragmatic Programmer?
 
J

James Edward Gray II

No worries there, James. I, too would like to post this somewhere
(writing a blog entry right now), but after trying to quote him, the
text suddenly feels lacking the punch I felt when I first read it. I
think this is due, in part, to the lack of context.

Do you think we can extract a general principle from what he's said?
Maybe something akin to the "tips" in The Pragmatic Programmer?

Ara knows and sees all? ;)

Or perhaps: Don't let fancy programming get in the way of good
programming.

James Edward Gray II
 
A

-a

(replying via google groups - sorry for any formatting issues)

I have not had such problems with exception traces. Emacs parses the
trace, so I've never had a problem tracking exceptions down.

well although that may sound heretical to me (vim user ;-)) i was
thinking more along the lines of debugging production code where you
typically just have some logs or, as i'm currently doing, debugging
stacktraces hand written and walked out of a classified facility!
(seriously)

You said you disagreed about variable names, but I didn't make an
argument about variable names. The argument was about needless
temporaries lying around and mucking up at-a-glance comprehension.
Those temporaries can be culled away nicely with Object#as.

expanded = stems.map { |stem|
"#{ stem }.#{ guess_extension stem }"
}.as { |guesses|
transform2(source_files + transform1(guesses))
}.as { |basenames|
basenames.map { |basename|
File.join dir, basename
}
}

Of course transform1 and transform2 are bad names --- they are meant
to be bad! They are intentionally generic for the purpose of the
example. They stand for any function.

i know. still, i couldn't resist.
You gave the Filelist example as the preferred code, which is exactly
right. However we are not talking about Filelist, but the
*implementation* of Filelist.

fair enough.
Could you clarify how the above code lends "strong support to bad
programming practices"? That seems unfounded. On the contrary, I
find it more appealing at least because (1) the intent of the code ---
to produce a value for 'expanded' --- is more obvious; (2) there are
no temporaries lying around which may potentially cause confusion in
future maintenance; (3) the logic flow is more obvious: three
consecutive operations, with the output of one fed into the input of
the next.

for the record, i would use #as just as i use #eval and co. as far as
clarity goes, maybe you come from a functional background, but i've
never found stringing lambdas together as very clear. i also
typically prefer

response = http.response
response.foo
response.bar

to

http.response.foo
http.response.bar

for two reasons

1) error reporting
2) vars are cheap in ruby, function calls are not

although for simple/short code i do use the second often, reserving
the former for 'real' code i anticipate maintaining and debugging. i
suspect i'm in the minority here.

anyhow - eval, instance_eval, class << self, are all tools that are
fugly, dangerous, and let us do things that probably should not be
aired in the open. #as, #returning, and company seem, to me, are
meant more as annotations, for example,

def foobar
returning Object.new do |object| ### hey! i'm returning an
object

but can easily obscure code too as scopes (potentially) and names
mogrify a few times in a line or two when they are chained.

i guess in the end i'm being more critical of the particular example
you gave rather than #as on the whole - but #as just seems to have a
little code smell too it since it loses much of it's power inside a
small method where local vars get popped anyhow

so

def foobar
tmp = self
someting_with tmp
end

vs

def foobar
self.as{|tmp| something_with tmp}
end

is obviously not much in the way of gained clarity.

it just seems like it's *only* in longish method chains begging for a
re-factoring that it really shines. i'd really have to use it anger
to decide.


cheers.
 
D

Daniel DeLorme

ara.t.howard said:
the very real problem with this sort of thing is that exceptions will
map to one fugly line, making debugging for some poor soul a nightmare.

That's just false. The backtrace will have the correct line number, no
matter how many lines an expression spans.
and of course this isn't even addressing the issue that anyone *really*
naming a function transform1 or transform2 should be smothered in their
sleep

+1, no one will argue with that, but it's unrelated to the issue at
hand. If your purpose was to poison the well[1] by pouring ridicule on a
tangential topic, shame on you. If not, I apologize.
i strongly disagree: variable names are one of the single most important
elements of writing maintainable code - unless you happen to be one of
the very few who loves writing documentation (i certainly don't).
variables being references in ruby, i consider it something almost evil
*not* to through in a well named variable where it lends clarity by
literally spelling out the programmer's intent, cuts line length, makes
transitioning or modifying the code later vastly easier, and stacktraces
actually meaningful

Well it's a good thing that you like variable names because Object#as is
*all* about using variables. So let's rewrite the example to use
*descriptive* variables names (and less lines, you seem to like less lines):

all_files = stems.
map{ |stem| stem + "." + guess_extension(stem) }.
as{ |guesses| transform2(source_files + transform1(guesses)) }.
map{ |basename| File.join(dir, basename) }

In the end, this is not a question of golfing or clarity or
maintainability or bad programming practices. It's simply a question of
style: imperative vs. functional. The only objective difference is that
using Object#as the temporary variable you create is isolated within its
own scope. The rest is an entirely subjective difference of style.

Daniel

[1] http://www.nizkor.org/features/fallacies/poisoning-the-well.html
 
F

furtive.clown

Let's compare them again. I changed some variable names which will
hopefully remove that red herring from the conversation. I also made
the styles more consistent for better comparison. (I was going to
show both my style and your one-liner style, but it was too
distracting.)

Temporaries in the scope of the target all_files:

data_files = stems.map { |stem|
"#{ stem }.#{ guess_extension stem }"
}
basenames =
add_suffixes(source_files + add_prefixes(data_files))
all_files = basenames.map { |basename|
File.join dir, basename
}

Temporaries inside block chains:

all_files = stems.map { |stem|
"#{ stem }.#{ guess_extension stem }"
}.as { |data_files|
add_suffixes(source_files + add_prefixes(data_files))
}.map { |basename|
File.join dir, basename
}

(BTW 'suffix' here means the chars right before the dot; after the dot
I call the extension.)

I almost editorialized the headings as "Temporaries floating around
randomly, obscuring the target all_files," and "Temporaries tucked
away safely inside block chains, leaving the lone target all_files for
all of us to see."
well although that may sound heretical to me (vim user ;-)) i was
thinking more along the lines of debugging production code where you
typically just have some logs or, as i'm currently doing, debugging
stacktraces hand written and walked out of a classified facility!
(seriously)

I use emacs with vi bindings; you might say I'm the product of a mixed
marriage. I get Hanukkah presents *and* Christmas presents,
metaphorically speaking.

The stacktrace argument holds no water with me. Is there a ruby bug
in the line-number reporting? I don't see the issue here.
for the record, i would use #as just as i use #eval and co. as far as
clarity goes, maybe you come from a functional background, but i've
never found stringing lambdas together as very clear.

This is part of my motivation here. The block-chains above are
beautiful to me. Concise, clear, and everything else.

Several years ago I experimented with writing ruby in a functional
style where it seemed appropriate. I loved the results. There is a
lot to say here. In short, I became a better ruby programmer (which I
didn't think was possible!). By functional style I mean functional
style in the small, such as inside the definition of a method.
Thinking in terms of transformations, removing or reducing side-
effects --- well, I won't get into it now.
i also typically prefer

response = http.response
response.foo
response.bar

to

http.response.foo
http.response.bar

That's not an example of functional style. That's using a local
variable verses not doing so. It has nothing to do with functional
style.

Part of your response attempts to conflate #as with #eval,
#instance_eval, #returning, singleton objects, and perhaps also the
kitchen sink. However we are talking about #as. I do not accept this
argument-by-association. If you wish to make an argument against #as,
you are obligated to address #as directly.
i guess in the end i'm being more critical of the particular example
you gave rather than #as on the whole - but #as just seems to have a
little code smell too it since it loses much of it's power inside a
small method where local vars get popped anyhow

On the contrary, every local variable removed from the target
variable's scope is a win. Even if it's one variable, that's a win.
That's one less distraction to the purpose of the code. Or two, such
as in the example above.

If a programmer is making huge, complex method definitions and refuses
to split them up, then there's nothing we can do about it. Your
argument seems to be, "Well, with #as, those huge definitions will
become more manageable, and therefore #as will encourage the
programmer not to split them up." It is difficult to explain the
ridiculousness in that line of thinking. In the meantime, good
programmers will correctly use smaller definitions while benefiting
from #as (or, at least I benefit from it).
def foobar
tmp = self
someting_with tmp
end

vs

def foobar
self.as{|tmp| something_with tmp}
end

Straw man. Nobody in their right mind would do that. That's not an
argument against #as.

I have yet to see a legitimate argument against #as here, save for the
clutter argument which also applies to #tap etc as well. I can
appreciate that to some degree, yet in many years I have not seen one
case of an actual problem arising from it.

My point is inherently difficult to make because it requires an
appreciation of method chaining and block-chaining. In the larger
context, an appreciation of functional style is also involved. So,
try it out for six months: if you still don't like it after that, then
you can return it for a full refund.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,764
Messages
2,569,567
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top