13

In Smalltalk there is the method ifNotNilDo: It is used like this:

database getUser ifNotNilDo: [:user | Mail sendTo: user ]

On objects that are not nil the block is executed, passing the object itself as a parameter. The implementation in class UndefinedObject (Smalltalk's equivalent of Ruby's NilClass) simply does nothing. That way, if getting the user resulted in a nil object, nothing would happen.

I am not aware of something similar for Ruby, so I rolled out my own solution. It goes like this:

class Object
  def not_nil
    yield(self)
  end
end

class NilClass
  def not_nil
    # do nothing
  end
end

It could be used like this:

users = {:peter => "peter@peter.com", :roger => "roger@roger.com" }
users[:roger].not_nil {|u| Mail.send(u) }

This saves us from accessing the hash twice

Mail.send(users[:roger]) if users[:roger]

... or using a temp-variable:

u = users[:roger]
Mail.send(u) if u

Update:

People are starting to suggest solutions based on hash-operations, and also accessing the hash twice. My question is not directly hash-related.

Imagine instead that the first operation is not a hash-access and also expensive. Like:

RemoteUserRepo.find_user(:roger).not_nil {|u| Mail.send(u) }

(end-of-update)

My questions are:

  • Am I wrong to re-invent this idiom?
  • Is there something like this (or better) supported in Ruby out-of-the-box?
  • Or is there another, shorter, more elegant way to do it?
Sebastian N.
  • 1,962
  • 15
  • 26
  • 1
    I'd personally go with `Mail.send(users[:roger]) if users[:roger]`. Accessing the hash twice shouldn't be that big of a deal in most situations – JKillian Oct 26 '14 at 20:23

6 Answers6

21

In Ruby 2.3.0+, you can use the safe navigation operator (&.) in combination with Object#tap:

users[:roger]&.tap { |u| Mail.send(u) }
ndnenkov
  • 35,425
  • 9
  • 72
  • 104
  • 1
    you're right. Sorry for the mistake, I deleted my comment to avoid confusing other people. – lcjury Jan 07 '23 at 19:12
7

In ActiveSupport there is try method. https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb

data = { a: [1,2,3] }
data[:a].try(:first)
#=> 1
data[:b].try(:first)
#=> nil
data[:b].first
#=> Exception

Under the hood it is implemented close to yours solution. For any object but nil it will "send a message" (in terms of Smalltalk) with attributes.

# object.rb
def try(*a, &b)
  if a.empty? && block_given?
    yield self
  else
    public_send(*a, &b) if respond_to?(a.first)
  end
end

# nilclass
def try(*args)
  nil
end

About your questions

Am I wrong to re-invent this idiom?

Rails guys have made something similar

Is there something like this (or better) supported in Ruby out-of-the-box?

No, Ruby doesn't support it out-of-the-box

Or is there another, shorter, more elegant way to do it?

In my opinion it has a problem: programmer should control data. One should know what kind of data he has and handle each type and each case, or raise an error. In your case it is valid for all data types but NilClass. What can lead to bugs that will very hard to debug.

I prefer to use old-fashioned

Mail.send(users[:roger]) if users[:roger]
# or
users[:roger] && Mail.send(users[:roger])
# or use caching if needed
fl00r
  • 82,987
  • 33
  • 217
  • 237
  • Thanks for answering my questions. It seems that `try` it *can* be used like in my approach... Pity it's Rails-only though... – Sebastian N. Oct 26 '14 at 20:54
  • `try` has nothing to do with Rails. It's in `ActiveSupport`. – Jörg W Mittag Oct 26 '14 at 21:51
  • 1
    You might want to mention the important change in `try` from Rails3 to Rails4: `try` used to just hide `nil` checks but now it also suppresses `NoMethodError`s from non-`nil` receivers, there is `try!` if you want just the v3 behavior. – mu is too short Oct 27 '14 at 00:04
4

You could use tap to avoid two Hash accesses:

users[:roger].tap { |u| Mail.send(u) if u }

I might use something like this:

[ users[:roger] ].reject(&:nil?).each { |u| Mail.send u }
Mark Reed
  • 91,912
  • 16
  • 138
  • 175
2

In functional languages like Ruby there's an idiomatic solution that takes advantage of the fact that assignment statements return values that can be tested:

unless (u = users[:roger]).nil?
  Mail.send(u)
end

You thus avoid the extra hash lookup, as desired. (Some functional purists disapprove of this sort of thing, however, as it tests the return value of a side-effecting statement.)

Alp
  • 2,766
  • 18
  • 13
  • Thanks for pointing this out. Seems cool. What would be the scope of `u`? Does it exist after the `end`? – Sebastian N. Oct 26 '14 at 20:48
  • 1
    Conditionals (`if`, `unless`) don't create a scope in Ruby, so `u` will just be a local in whatever the present lexical scope is and will still have a binding after the `end`. – Alp Oct 26 '14 at 20:49
  • 2
    Yes, the `u` will continue to exist after the `end`.. which is one of the main reasons I would tend to prefer `tap` in this situation. – Mark Reed Oct 26 '14 at 21:02
  • 1
    You don't need 'nil?' in condition – fl00r Oct 26 '14 at 21:49
  • 2
    In the most general case, one does need the `nil?` in the condition, to distinguish `false` and `nil` returns. – hemflit Nov 28 '14 at 14:52
  • Almost a decade later, here I am learning that Ruby conditional blocks don't create scope. I hadn't noticed! Thanks for pointing that out, @Alp! – JakeRobb Apr 12 '23 at 17:21
2

No, you are not wrong to re-invent this idiom. You might still do better to give it a more accurate name, perhaps if_not_nil.

Yes, there is a core Ruby way to do this, though it's not exactly oozing with elegance:

[RemoteUserRepo.find_user(:roger)].compact.each {|u| Mail.send(u)}

Recall that compact returns an array's copy with the nil elements removed.

hemflit
  • 2,819
  • 3
  • 22
  • 17
  • 1
    Reminds me of Scala's Option, which is to be treated as an array of maximum 1 element. You then eventually access the element with "map" or "foreach". Makes sense, but never totally liked it. – Sebastian N. Nov 28 '14 at 23:37
  • That Scala thing sounds like a pretty neat way to implement Option! Makes it trivial but unambiguous to deal with Option(Option(MyType)) as well. – hemflit Dec 11 '14 at 18:56
1
users.delete_if { |_, email| email.nil? }.each { |_, email| Mail.send email }

or

users.values.compact.each { |email| Mail.send email }
Mori
  • 27,279
  • 10
  • 68
  • 73
  • 1
    `delete_if` will mutate original array what can case some problems. Use `select`/`reject` instead – fl00r Oct 26 '14 at 20:31