Search code examples
rubysafe-navigation-operator

Does Ruby safe navigation operator evaluate its parameters when its receiver is nil?


Question:

Does Ruby safe navigation operator (&.) evaluate its parameters when its receiver is nil?

For example:

logger&.log("Something important happened...")
  • Is the "Something important happened..." string evaluated here?
  • Could you provide an authoritative source, which proves or denies this fact?
  • Or suggest a way how to check it?

Thanks in advance.




Why I am looking for an answer to this question?

I have the code like the following throughout my codebase:

logger.log("Something important happened. (#{Time.current})") if verbose

My main goal is to remove the repetition of the if verbose check whenever I call the log method since it is easy to forget about it and you will be not notified at all about the misusage.

Inspired by the Tell, don't ask principle,

I have moved if verbose check inside log method implementation.

class Logger
  # ...
  
  def log(message)
    return unless verbose

    # ...
  end
end

def logger
  @logger ||= Logger.new
end

logger.log("Something important happened. (#{Time.current})")

This approach simplified my code since I have solved my main problem - I don't need to remember to place if verbose whenever I call the log method,

but I have received another issue.

"Something important..." string is always evaluated, no matter whether verbose is true or false.

Therefore, I have completely changed the solution:

  • logger returns nil when verbose is false.
  • Ruby safe navigation operator should be used in front of log calls.
def logger
  @logger ||= Logger.new if verbose
end

logger&.log("Something important happened. (#{Time.current})")

As a result, I have replaced the initial problem of remembering if verbose checks to remembering of &. calls.

But, anyway, I consider this as an improvement, since forgetting to utilize the safe navigation operator raises the NoMethodError, in other words, notifies about the log method misusage.

So now, in order to be sure that the 'safe navigation operator approach' is actually a 'better' option for my problem,

I need to know exactly whether the safe navigation operator in Ruby evaluates its parameters when its receiver is nil.


Solution

  • To quote from the syntax documentation for the safe navigation operator:

    &., called “safe navigation operator”, allows to skip method call when receiver is nil. It returns nil and doesn't evaluate method's arguments if the call is skipped.

    As such, the arguments of your log method are not evaluated if the logger is nil when you call it as

    logger&.log("something happened at #{Time.now}")
    

    With that being said, note that the Ruby core logger offers a different solution to your exact issue, namely to avoid having to evaluate potentially expensive arguments if the log level is to high.

    The Ruby core logger implements its add method something like this (simplified):

    class Logger
      attr_accessor :level
    
      def initialize(level)
        @level = level.to_i
      end
    
      def add(severity, message = nil)
        return unless severity >= level
        
        message ||= yield
        log_device.write(message)
      end
    
      def info(message = nil, &block)
        add(1, message, &block)
      end
    end
    

    You can then use this as

    logger = Logger.new(1)
    logger.info { "something happened at #{Time.now}" }
    

    Here, the block is only evaluated if the log level is high enough that the message is actually used.