[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

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

furtive.clown

11/13/2007 6:39:00 AM

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

35 Answers

furtive.clown

11/13/2007 7:08:00 AM

0


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.

Robert Klemme

11/13/2007 8:12:00 AM

0

2007/11/13, furtive.clown@gmail.com <furtive.clown@gmail.com>:
>
> 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

--
use.inject do |as, often| as.you_can - without end

furtive.clown

11/13/2007 9:07:00 AM

0

On Nov 13, 3:12 am, Robert Klemme <shortcut...@googlemail.com> wrote:
>
> 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


Robert Klemme

11/13/2007 10:25:00 AM

0

2007/11/13, furtive.clown@gmail.com <furtive.clown@gmail.com>:
> On Nov 13, 3:12 am, Robert Klemme <shortcut...@googlemail.com> wrote:
> >
> > 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.

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

--
use.inject do |as, often| as.you_can - without end

Daniel DeLorme

11/13/2007 10:40:00 AM

0

furtive.clown@gmail.com wrote:
> 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

Robert Klemme

11/13/2007 10:48:00 AM

0

2007/11/13, Daniel DeLorme <dan-ml@dan42.com>:
> furtive.clown@gmail.com wrote:
> > 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?

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

--
use.inject do |as, often| as.you_can - without end

Daniel DeLorme

11/13/2007 11:39:00 AM

0

Robert Klemme wrote:
> 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


Robert Klemme

11/13/2007 11:59:00 AM

0

2007/11/13, Daniel DeLorme <dan-ml@dan42.com>:
> Robert Klemme wrote:
> > 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*?

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.

> > 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.

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

--
use.inject do |as, often| as.you_can - without end

furtive.clown

11/13/2007 1:44:00 PM

0

On Nov 13, 5:39 am, Daniel DeLorme <dan...@dan42.com> wrote:
> 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

ara.t.howard

11/13/2007 7:04:00 PM

0


On Nov 13, 2007, at 12:10 AM, furtive.clown@gmail.com wrote:

> 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://codeforp...
--
it is not enough to be compassionate. you must act.
h.h. the 14th dalai lama