The Safe Navigation Operator (&.) in Ruby

 

http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/

 

 

The most interesting addition to Ruby 2.3.0 is the Safe Navigation Operator(&.). A similar operator has been present in C# and Groovy for a long time with a slightly different syntax – ?.. So what does it do?

Scenario

Imagine you have an account that has an owner and you want to get the owner’s address. If you want to be safe and not risk a nil error, you would write something like the following:

if account && account.owner && account.owner.address
...
end

This is really verbose and annoying to type. ActiveSupport includes the try method which has a similar behaviour (but with few key differences that will be discussed later):

if account.try(:owner).try(:address)
...
end

It accomplishes the same thing – it either returns the address or nil if some value along the chain is nil. The first example may also return false if, for example, the owner is set to false.

Using the safe navigation operator (&.)

We can rewrite the previous example using the safe navigation operator:

account&.owner&.address

The syntax is a bit awkward but I guess we will have to deal with it because it does make the code more compact.

More examples

Let’s compare all three approaches in more detail.

account = Account.new(owner: nil) # account without an owner

account.owner.address
# => NoMethodError: undefined method `address' for nil:NilClass

account && account.owner && account.owner.address
# => nil

account.try(:owner).try(:address)
# => nil

account&.owner&.address
# => nil

No surprises so far. What if owner is false (unlikely but not impossible in the exciting world of shitty code)?

account = Account.new(owner: false)

account.owner.address
# => NoMethodError: undefined method `address' for false:FalseClass `

account && account.owner && account.owner.address
# => false

account.try(:owner).try(:address)
# => nil

account&.owner&.address
# => undefined method `address' for false:FalseClass`

Here comes the first surprise – the &. syntax only skips nil but recognizes false! It is not exactly equivalent to the s1 && s1.s2 && s1.s2.s3 syntax.

What if the owner is present but doesn’t respond to address?

account = Account.new(owner: Object.new)

account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>

account && account.owner && account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`

account.try(:owner).try(:address)
# => nil

account&.owner&.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`

Oops, the try method doesn’t check if the receiver responds to the given method. This is why it’s always better to use the stricter version of try – try!:

account.try!(:owner).try!(:address)
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`

Pitfalls

nil.nil?

# => true

nil?.nil?
# => false

nil&.nil?
# => nil

As Joeri Samson pointed out in the comments, this section is actually wrong – I mistakenly used ?. instead of &.. But I still think that the last example is confusing and nil&.nil? should return true.

Array#dig and Hash#dig

The #dig method is, in my opinion, the most useful feature in this version. No longer do we have to write abominations like the following:

address = params[:account].try(:[], :owner).try(:[], :address)

# or

address = params[:account].fetch(:owner) { {} }.fetch(:address)

We can now simply use Hash#dig and accomplish the same thing:

address = params.dig(:account, :owner, :address)

Final words

I really dislike dealing with nil values in dynamic languages (check my previous posts) and think the addition of the safe operator and the dig methods are really neat.

 

http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/

do enums work with indifferent access

 

Yes
Implements a hash where keys :foo and "foo" are considered to be the same.
enum provider_name: {
trade_me: ‘Trade Me’,
facebook: ‘Facebook’,
google: ‘Google’
}
irb(main):010:0> provider = ‘google’
=> “google”
irb(main):011:0> provider.to_s
=> “google”
irb(main):012:0> provider.to_sym
=> :google
irb(main):013:0> ThirdPartyIdentity.provider_names[provider.to_sym]
=> “Google”
irb(main):014:0> ThirdPartyIdentity.provider_names[provider.to_s]
=> “Google”
https://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html
rgb = ActiveSupport::HashWithIndifferentAccess.new

rgb[:black] = '#000000'
rgb[:black]  # => '#000000'
rgb['black'] # => '#000000'

rgb['white'] = '#FFFFFF'
rgb[:white]  # => '#FFFFFF'
rgb['white'] # => '#FFFFFF'

Internally symbols are mapped to strings when used as keys in the entire writing interface (calling []=merge, etc). This mapping belongs to the public interface. For example, given:

hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)

You are guaranteed that the key is returned as a string:

hash.keys # => ["a"]

Technically other types of keys are accepted:

hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
hash[0] = 0
hash # => {"a"=>1, 0=>0}

but this class is intended for use cases where strings or symbols are the expected keys and it is convenient to understand both as the same. For example the params hash in Ruby on Rails.

Note that core extensions define Hash#with_indifferent_access:

rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access

which may be handy.

To access this class outside of Rails, require the core extension with:

require "active_support/core_ext/hash/indifferent_access"

which will, in turn, require this file.

 

time difference

the time difference in months can be treated as:
`((Time.current – updated_at)/1.month.seconds).to_i`
This will give you:

“`
2.4.5 :013 > ((Time.current – 1.month.ago)/1.month.seconds).to_i
=> 1
2.4.5 :014 > ((Time.current – 10.month.ago)/1.month.seconds).to_i
=> 10
2.4.5 :015 > ((Time.current – 0.month.ago)/1.month.seconds).to_i
=> 0
“`
Also there is a good gem more human style like https://github.com/tmlee/time_difference a bit old but does the job.

prefer `Time.current` because this guy decides if it wants to be a `Time.zone.now` or a `Time.now` depending on the project settings.
This is also valid for `Date.current` and `DateTime.current`
a great explanation here https://nandovieira.com/working-with-dates-on-ruby-on-rails