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.

d11wtq
  • 34,788
  • 19
  • 120
  • 195
  • 3
    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. – Erik Hinton Oct 24 '11 at 16:31
  • 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 :) – d11wtq Oct 24 '11 at 16:35
  • 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. – d11wtq Oct 24 '11 at 16:38
  • 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`. – Kache Jun 20 '14 at 21:51

6 Answers6

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
tokland
  • 66,169
  • 13
  • 144
  • 170
  • 2
    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. – d11wtq Oct 24 '11 at 17:17
  • @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: http://ick.rubyforge.org/ – tokland Oct 24 '11 at 17:35
  • 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 :) – d11wtq Oct 25 '11 at 02:10
  • I will accept your answer, though I'll give it another few hours to see if anybody else has any thoughts. – d11wtq Oct 25 '11 at 02:11
  • 1
    The `Object#as` method should totally be in the sources. So many situations where it becomes useful! (I asked about it there: http://stackoverflow.com/questions/26378890/ruby-reuse-value-in-a-block-without-assigning-it-to-variable-write-object-meth) – Augustin Riedinger Oct 15 '14 at 09:42
  • Found it! It is `BasicObject#instance_eval` method: http://www.ruby-doc.org/core-2.1.3/BasicObject.html#method-i-instance_eval (though the name is ugly and `as` sounds much better!) – Augustin Riedinger Oct 15 '14 at 09:58
  • @AugustinRiedinger, `instance_eval` is different, it changes the context of `self`. `as` simply yields the object to the block. – tokland Oct 15 '14 at 10:07
  • 4
    Ruby 2.5 gets `yield_self` method which is basically the `as` implemented here. This is mentioned in @kuboon's answer also. – RajaRaviVarma Jun 14 '18 at 13:29
  • Maybe should edit my answer to also note it, being the accepted one. – tokland Jun 14 '18 at 14:29
13
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://stackoverflow.com/a/47890832/683157

kuboon
  • 9,557
  • 3
  • 42
  • 32
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

Matthias Michael Engh
  • 1,159
  • 2
  • 10
  • 25
  • `yield_self` is now in [Kernel](https://ruby-doc.org/core/classes/Kernel.html#method-i-yield_self) and also known as [`then`](https://ruby-doc.org/3.2.2/Kernel.html#method-i-then). – mu is too short Jul 25 '23 at 16:40
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
Tyler Rick
  • 9,191
  • 6
  • 60
  • 60
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
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)
smendola
  • 2,273
  • 1
  • 15
  • 14