Forum: Ruby tap { break } idiom deserves its own Kernel method?

9a0171f862db7f9d94e3dfdbc5ebc7c5?d=identicon&s=25 Andy Lowry (andrewlowry)
on 2013-07-22 19:37
I use this idiom from time to time:

  x = expr.tap{|value| break expr-involving-value)}

It works well, but reading it requires the reader to understand a
relatively obscure part of ruby, namely the behavior of break during a
yield.

Here's my most recent case where the idiom useful: I've got a list of
model object ids, and I want produce an array of the model objects in
the order their ids appear in that list.

Obviously, this works:

   id_list.map{|id| Model.find(id)}

Here's an approach that does a single query:

   unsorted = Model.where(id: id_list)
   objs_map = unsorted.reduce(Hash.new) {|h,o| h[o.id] = o; h}
   sorted = id_list.map{|id| objs_map[id]}

Here's the same thing using tap/break, to make it more obvious that the
only thing I'm really interested in is that final value "sorted":

   sorted = Model.where(id: id_list).tap do |unsorted|
     break unsorted.reduce(Hash.new) {|h,o| h[o.id] = o; h}
   end.tap do |objs_hash|
     break id_list.map{|id| objs_hash[id]}
   end

I find enough uses for this idiom that I'm thinking it'd be worth giving
it a name and its own Kernel method, rather than forcing the use of a
relatively narrowly understood language feature (behavior of break
during yield). "pipe" or "transform" may be a good name for it.
Definition would be almost identical to that of tap.

  Kernel.module_eval do
    def pipe
      yield self
    end
  end

Then the example above turns into:

  sorted = Model.where(id: id_list).pipe do |unsorted|
    unsorted.reduce(Hash.new) {|h,o| h[o.id] = o; h}
  end.pipe do |objs_map|
    id_list.map{|id| objs_hash[id]}
  end

Any thoughts?
E0d864d9677f3c1482a20152b7cac0e2?d=identicon&s=25 Robert Klemme (robert_k78)
on 2013-07-22 22:14
(Received via mailing list)
On Mon, Jul 22, 2013 at 7:37 PM, Andy Lowry <lists@ruby-forum.com>
wrote:

> the order their ids appear in that list.
>
> it a name and its own Kernel method, rather than forcing the use of a
> Then the example above turns into:
>
>   sorted = Model.where(id: id_list).pipe do |unsorted|
>     unsorted.reduce(Hash.new) {|h,o| h[o.id] = o; h}
>   end.pipe do |objs_map|
>     id_list.map{|id| objs_hash[id]}
>   end
>
> Any thoughts?
>

 I would choose a much less arcane solution:

objs_map = {}
Model.where(id: id_list).each {|o| objs_map[o.id] = o}
sorted = id_list.map{|id| objs_map[id]}

If id_list is smallish (< 30 or so) then index lookup might actually be
faster - or at least fast enough.

sorted = Model.where(id: id_list).sort_by {|o| id_list.index(o)}

Kind regards

robert
A74a68807619459925cc1d8e1045c7bd?d=identicon&s=25 Tony Arcieri (Guest)
on 2013-07-22 22:27
(Received via mailing list)
On Mon, Jul 22, 2013 at 10:37 AM, Andy Lowry <lists@ruby-forum.com>
wrote:

> I use this idiom from time to time:
>
>   x = expr.tap{|value| break expr-involving-value)}
>

How is this any different than:

    x = expr-involving-value

?
9a0171f862db7f9d94e3dfdbc5ebc7c5?d=identicon&s=25 Andy Lowry (andrewlowry)
on 2013-07-22 22:38
This is my first code sample but with one temporary variable removed.
Getting rid of the remaining temp variable is much more difficult. You
could inline the "Model.where..." expression but then you'd end up
executing it repeatedly in the id_list loop. The reason I use the idiom
is to avoid cluttering my namespace with names for things that are of
ephemeral interest, i.e. only useful as a step in achieving some needed
value. In my opinion, those values are less prominent in the "pipe"
version of my code than in either my first code sample or in your code,
and that seems valuable to me.

The only thing that seems arcane to me in this thread is using break
inside tap; as I pointed out, it's something a lot of ruby coders won't
understand. Code blocks with well-defined behaviors are obviously well
understood by any ruby developer who's not a total novice. I believe
that "pipe" would take a useful idiom that is seen occasionally (not
just in my code) and is arcane because of the tap/break combo, and
provide a non-arcane alternative.

Andy

Robert Klemme wrote in post #1116270:
> On Mon, Jul 22, 2013 at 7:37 PM, Andy Lowry <lists@ruby-forum.com>
> wrote:
>
>> the order their ids appear in that list.
>>
>> it a name and its own Kernel method, rather than forcing the use of a
>> Then the example above turns into:
>>
>>   sorted = Model.where(id: id_list).pipe do |unsorted|
>>     unsorted.reduce(Hash.new) {|h,o| h[o.id] = o; h}
>>   end.pipe do |objs_map|
>>     id_list.map{|id| objs_hash[id]}
>>   end
>>
>> Any thoughts?
>>
>
>  I would choose a much less arcane solution:
>
> objs_map = {}
> Model.where(id: id_list).each {|o| objs_map[o.id] = o}
> sorted = id_list.map{|id| objs_map[id]}
>
> If id_list is smallish (< 30 or so) then index lookup might actually be
> faster - or at least fast enough.
>
> sorted = Model.where(id: id_list).sort_by {|o| id_list.index(o)}
>
> Kind regards
>
> robert
15000f55138ae94b0f362ed7c625461a?d=identicon&s=25 unknown (Guest)
on 2013-07-22 22:49
(Received via mailing list)
Am 22.07.2013 19:37, schrieb Andy Lowry:
> the order their ids appear in that list.
>
> it a name and its own Kernel method, rather than forcing the use of a
> Then the example above turns into:
>
>    sorted = Model.where(id: id_list).pipe do |unsorted|
>      unsorted.reduce(Hash.new) {|h,o| h[o.id] = o; h}
>    end.pipe do |objs_map|
>      id_list.map{|id| objs_hash[id]}
>    end
>
> Any thoughts?

To me this looks like defining a method for doing method chaining...?!

IMO, `tap' is overused or often even misused anyway, but probably
I only think that because I do not get its many advantages... :)
But what I'm sure of is that the above tap-break idiom is too
cryptic for me.

Regards,
Marcus
9a0171f862db7f9d94e3dfdbc5ebc7c5?d=identicon&s=25 Andy Lowry (andrewlowry)
on 2013-07-22 22:49
Tony Arcieri wrote in post #1116271:
> On Mon, Jul 22, 2013 at 10:37 AM, Andy Lowry <lists@ruby-forum.com>
> wrote:
>
>> I use this idiom from time to time:
>>
>>   x = expr.tap{|value| break expr-involving-value)}
>>
>
> How is this any different than:
>
>     x = expr-involving-value
>
> ?

It's the tap that makes the variable "value" available for use in
expr-involving-value.

This really mostly comes down to a way to avoid creating method-scoped
variables to hold temporary values. The block parameters end up taking
their place, which in my opinion makes them less prominent in the code,
as they should be.

Obviously there's nothing earth-shattering going on here. Just something
I believe would be a nice and very small addition.
9a0171f862db7f9d94e3dfdbc5ebc7c5?d=identicon&s=25 Andy Lowry (andrewlowry)
on 2013-07-22 22:54
unknown wrote in post #1116273:
> But what I'm sure of is that the above tap-break idiom is too
> cryptic for me.
>
> Regards,
> Marcus

I agree! But I use it, because I like the pattern when it fits, and it's
what's available. I'd prefer to use the same pattern with something less
cryptic like my pipe proposal.
15000f55138ae94b0f362ed7c625461a?d=identicon&s=25 unknown (Guest)
on 2013-07-22 22:56
(Received via mailing list)
Am 22.07.2013 22:38, schrieb Andy Lowry:
> The reason I use the idiom
> is to avoid cluttering my namespace with names for things that are of
> ephemeral interest, i.e. only useful as a step in achieving some needed
> value.

I would always value easily readable code higher than an
"uncluttered" namespace, but tastes are different.

Regards,
Marcus
A74a68807619459925cc1d8e1045c7bd?d=identicon&s=25 Tony Arcieri (Guest)
on 2013-07-22 23:02
(Received via mailing list)
On Mon, Jul 22, 2013 at 1:49 PM, Andy Lowry <lists@ruby-forum.com>
wrote:

> This really mostly comes down to a way to avoid creating method-scoped
> variables to hold temporary values.


Sounds like you want a let binding
7223c62b7310e164eb79c740188abbda?d=identicon&s=25 Xavier Noria (fxn)
on 2013-07-22 23:06
(Received via mailing list)
I like tap itself on most occasions.

The point of tap for me is being declarative, you are telling the reader
the intention upfront. The alternative without tap for him is to *infer*
from the listing.

At first sight I don't quite see #pipe though, it basically moves a
variable to a block parameter. Not sure it is worth a new idiom.
9a0171f862db7f9d94e3dfdbc5ebc7c5?d=identicon&s=25 Andy Lowry (andrewlowry)
on 2013-07-22 23:45
I just used the pattern again, and this time it's a much simpler example
than the one I gave before. So maybe it will help.

The issue is I'm getting an integer parameter from a form, and I want to
store it as an integer or (if the parameter was empty) as nil.

So I've written this:

  x = params[:distance].tap{|d| break d.empty? ? nil : d.to_i}

I'd prefer to write this:

  x = params[:distance].pipe{|d| d.empty? ? nil : d.to_i}

Without these I'd need to either repeat the params[:distance] expresion,
as in the slightly damp

  x = params[:distance].empty? ? nil : params[:distance].to_i

or I'd resort to a temp variable as in

  dparam = params[:distance]
  x = dparam.empty? ? nil : dparam.to_i

Again, nothing earth moving here. I'm just partial to the pipe approach,
and thought perhaps others would like it too.
3df767279ce7d81db0a5bb30f5136863?d=identicon&s=25 Matthew Kerwin (mattyk)
on 2013-07-22 23:50
(Received via mailing list)
Incidentally, I think this feature request may be in line with what
you want: https://bugs.ruby-lang.org/issues/6721 (also look at #6373,
comment 24)
9a0171f862db7f9d94e3dfdbc5ebc7c5?d=identicon&s=25 Andy Lowry (andrewlowry)
on 2013-07-23 00:11
Thanks - you're right, it's exactly what I'm proposing.

Matthew Kerwin wrote in post #1116288:
> Incidentally, I think this feature request may be in line with what
> you want: https://bugs.ruby-lang.org/issues/6721 (also look at #6373,
> comment 24)
Please log in before posting. Registration is free and takes only a minute.
Existing account

NEW: Do you have a Google/GoogleMail, Yahoo or Facebook account? No registration required!
Log in with Google account | Log in with Yahoo account | Log in with Facebook account
No account? Register here.