Combinatory method like tap, but able to return a different value?
Asked Answered
U

6

33

I'm going through a phase of trying to avoid temporary variables and over-use of conditional where I can use a more fluid style of coding. I've taken a great liking to using #tap in places where I want to get the value I need to return, but do something with it before I return it.

def fluid_method
  something_complicated(a, b, c).tap do |obj|
    obj.update(:x => y)
  end
end

Vs. the procedural:

def non_fluid_method
  obj = something_complicated(a, b, c)
  obj.update(:x => y)
  obj # <= I don't like this, if it's avoidable
end

Obviously the above examples are simple, but this is a pretty common coding style in the ruby community nonetheless. I'll sometimes use #inject to pass an object through a series of filters too:

things.inject(whatever) do |obj, thing|
  thing.filter(obj)
end

Vs. the procedural:

obj = whatever
things.each do |thing|
  obj = thing.filter(obj)
end
obj

Now I'm facing repeated use of a condition like the following, and looking for a more fluid approach to handling it:

def not_nice_method
  obj = something_complex(a, b, c)
  if a_predicate_check?
    obj.one_more_method_call
  else
    obj
  end
end

The (slightly) cleaner solution is to avoid the temporary variable at the cost of duplication:

def not_nice_method
  if a_predicate_check?
    something_complex(a, b, c).one_more_method_call
  else
    something_complex(a, b, c)
  end
end

I can't help but feeling the desire to use something almost like #tap here though.

What other patterns might I follow here. I realise this is all just nonsensical sugar to some people and that I should just move onto more interesting problems, but I'm trying to learn to write in a more functional style, so I'm just curious what long-term rubyists have determined to be good ways to tackle situations like this. These examples are hugely simplified.

Upsweep answered 24/10, 2011 at 16:21 Comment(4)
At the risk of being pedantic, it seems that your use of tap to induce side-effects is anti-functional. Functional programmers and languages avoid or prevent side-effects. The point of tap is that it won't return what gets executed in it. Thus, it can be used two ways: debugging and methods that induce side-effects. The functional way is simply to chain methods together or composite them.Antonantone
No risk, I'd like to talk theory, though I fear this thread would be closed if there's no direct question, however, given that #update would return a boolean, not the value of obj (which is beyond my control), doesn't tap solve the need for a third expression to return the original value? I would like to understand more correct functional techniques :)Upsweep
Ah, I see how I could change that: update(something_complex(a, b, c)), where I have defined update to do argument.update(:x => y)... though this gets more verbose as the update parameters need passing in.Upsweep
Well, it'd just be syntactic sugar for writing a lambda and calling it. You could us it to change a value in the middle of a chain, like 3.tweak { |i| i * 2 } # returns 6 which is equivalent to lambda { 3 * 2 }.call. It would also let you create and use references mid-chain: 5.tweak { |id| obj = expensive_lookup(id); obj.ready? && obj.valid? } which is equiv to lambda { obj = lookup(id); obj.ready? && obj.valid? }.call.Krys
S
14

Define Object#as:

class Object
  def as
    yield self
  end
end

And now you can write:

def not_sure_this_is_nice_enough_method1
  something_complex(a, b, c).as do |obj| 
    a_predicate_check? ? obj.one_more_method_call : obj
  end
end
Strange answered 24/10, 2011 at 16:54 Comment(9)
Ohhh, your Object#as trick is definitely better (to my eyes) than the assign; if..else..end version. You could probably have an some_method(a, b).on(a_predicate?) { |obj| obj.whatever } too. I may take your as and reverse it slightly, adding with to the caller: with(something_complex(a, b)) { |obj| a_predicate? obj.whatever : obj }. Hmmm.Upsweep
@d11wtq: Indeed, the on { } looks also nice, it's usually better to call a method instead of concealing it with send(:methods) wrappers (the problem is you must give it name). Note that your with(x) { |x| ... } is in fact Ick's let: ick.rubyforge.orgStrange
Ick looks interesting, though I suspect it won't play well with RSpec, since it has a let method of its own, behaving completely differently :)Upsweep
I will accept your answer, though I'll give it another few hours to see if anybody else has any thoughts.Upsweep
The Object#as method should totally be in the sources. So many situations where it becomes useful! (I asked about it there: #26379390)Bander
Found it! It is BasicObject#instance_eval method: ruby-doc.org/core-2.1.3/BasicObject.html#method-i-instance_eval (though the name is ugly and as sounds much better!)Bander
@AugustinRiedinger, instance_eval is different, it changes the context of self. as simply yields the object to the block.Strange
Ruby 2.5 gets yield_self method which is basically the as implemented here. This is mentioned in @kuboon's answer also.Tiga
Maybe should edit my answer to also note it, being the accepted one.Strange
P
14
def best_nice_method
  something_complex(a, b, c).tap |obj|
    break obj.one_more_method_call if a_predicate_check?
  end
end

The magic is break in tap returns another value.

new

ruby 2.5 has yield_self which exactly you want. https://mcmap.net/q/424392/-difference-between-kernel-yield_self-yield-self-kernel-then-and-object-tap-in-ruby

Premonition answered 1/11, 2017 at 2:29 Comment(1)
Ruby 2.6 adds the alias #then to #yield_self.Scarlatina
C
8

instance_eval can be misused for this purpose

"this".instance_eval { |test| test + " works" }

since 2.5 it is possible to use yield_self

"easy".yield_self{ |a| a + " peasy" }

Read more:

https://ruby-doc.org/core-1.9.3/BasicObject.html#method-i-instance_eval

https://ruby-doc.org/core-2.5.0/Object.html#method-i-yield_self

Charming answered 6/9, 2018 at 21:26 Comment(1)
yield_self is now in Kernel and also known as then.Potsdam
K
3

I found a method in the Facets gem that might be what you were looking for: Kernel#ergo

So your original method:

def not_nice_method
  obj = something_complex(a, b, c)
  if a_predicate_check?
    obj.one_more_method_call
  else
    obj
  end
end

might end up looking something like this:

require 'facets/kernel/ergo'

def nice_method
  something_complex(a, b, c).ergo do |_| 
    a_predicate_check? ? _.one_more_method_call : _
  end
end
Kopp answered 17/1, 2013 at 1:59 Comment(0)
Q
1

I needed to do something like this and I like tokland's answer, but I didn't want to pollute Object for the small script I was writing. Instead, I made use of tap on an array:

[something_complicated].tap { |s| s[0] = new_cool_thing)}.first
Quillet answered 5/11, 2015 at 0:17 Comment(1)
easier to [bla].collect { |bla| "bla" }.firstCharming
H
0
class Object
  def apply_if(pred)
    if pred
      yield self
    else
      self
    end
  end
end

Typical usage:

      def rlrs_usage_by_group(group_id, date_range = nil)
        views = ContentView.joins(user: [:groups])
                           .where(groups: { id: group_id })
                           .where(viewable_type: 'RealStory')
                           .apply_if(date_range) {
                             _1.where(viewed_at: date_range)
                           }
      end

Your case:

def nice_method
  something_complex(a, b, c)
    .apply_if(a_predicate_check?) { 
      _1.one_more_method_call
    }
end

or even

  something_complex(a, b, c)
     .apply_if(a_predicate_check, &:one_more_method_call)
Hustings answered 25/7, 2023 at 15:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.