Does Ruby safe navigation operator (&.
) evaluate its parameters when its receiver is nil
?
For example:
logger&.log("Something important happened...")
"Something important happened..."
string evaluated here?Thanks in advance.
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
.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
.
To quote from the syntax documentation for the safe navigation operator:
&.
, called “safe navigation operator”, allows to skip method call when receiver isnil
. It returnsnil
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.
The argument to logger&.log
isn't evaluated when logger.is_a?(NilClass) == true
. Every Ruby expression that's evaluated should have an impact, so consider:
test = 1
nil&.log(test+=1); test
#=> 1
If the argument were evaluated by the interpreter, test would equal two. So, while the parser certainly parses the expression in your argument, it doesn't execute the inner expression.
You can verify what the parser sees with Ripper#sexp:
require 'ripper'
test = 1
pp Ripper.sexp "nil&.log(test+=1)"; test
[:program, [[:method_add_arg, [:call, [:var_ref, [:@kw, "nil", [1, 0]]], [:@op, "&.", [1, 3]], [:@ident, "log", [1, 5]]], [:arg_paren, [:args_add_block, [[:opassign, [:var_field, [:@ident, "test", [1, 9]]], [:@op, "+=", [1, 13]], [:@int, "1", [1, 15]]]], false]]]]] #=> 1
This clearly shows that the parser sees the incremented assignment in the symbolic expression tree. However, the assignment is never actually executed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With