Conditionally execute block in Ruby if value is not nil? (aka Smalltalk's ifNotNilDo:)
Asked Answered
M

6

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 => "[email protected]", :roger => "[email protected]" }
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?
Metzgar answered 26/10, 2014 at 20:13 Comment(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 situationsMiraflores
A
8

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
Abhorrent answered 26/10, 2014 at 20:27 Comment(3)
Thanks for answering my questions. It seems that try it can be used like in my approach... Pity it's Rails-only though...Metzgar
try has nothing to do with Rails. It's in ActiveSupport.Acescent
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 NoMethodErrors from non-nil receivers, there is try! if you want just the v3 behavior.Stowers
N
22

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

users[:roger]&.tap { |u| Mail.send(u) }
Nickerson answered 15/1, 2016 at 20:16 Comment(1)
you're right. Sorry for the mistake, I deleted my comment to avoid confusing other people.Clipping
A
8

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
Abhorrent answered 26/10, 2014 at 20:27 Comment(3)
Thanks for answering my questions. It seems that try it can be used like in my approach... Pity it's Rails-only though...Metzgar
try has nothing to do with Rails. It's in ActiveSupport.Acescent
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 NoMethodErrors from non-nil receivers, there is try! if you want just the v3 behavior.Stowers
M
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 }
Mas answered 26/10, 2014 at 20:18 Comment(1)
Thanks for mentioning tap. Didn't know about it.Metzgar
L
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.)

Lawlor answered 26/10, 2014 at 20:46 Comment(6)
Thanks for pointing this out. Seems cool. What would be the scope of u? Does it exist after the end?Metzgar
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.Lawlor
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.Mas
You don't need 'nil?' in conditionAbhorrent
In the most general case, one does need the nil? in the condition, to distinguish false and nil returns.Edelsten
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!Narda
E
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.

Edelsten answered 28/11, 2014 at 14:48 Comment(2)
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.Metzgar
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.Edelsten
B
1
users.delete_if { |_, email| email.nil? }.each { |_, email| Mail.send email }

or

users.values.compact.each { |email| Mail.send email }
Blastomere answered 26/10, 2014 at 20:27 Comment(1)
delete_if will mutate original array what can case some problems. Use select/reject insteadAbhorrent

© 2022 - 2024 — McMap. All rights reserved.