6

Why and how to use Ruby’s Hash#fetch for default values

 3 years ago
source link: https://avdi.codes/why-and-how-to-use-rubys-hashfetch-for-default-values/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Why and how to use Ruby’s Hash#fetch for default values – avdi.codes
swatch
Video Thumbnail
Video Thumbnail

Here’s another classic from the early days of RubyTapas. Originally published as Episode #11 in October 2012, it’s a complement to the episode on using fetch as an assertion. This episode digs into the difference between using the || operator for defaults vs. using Hash#fetch.

swatch
Video Thumbnail
Video Thumbnail

Director’s commentary: I can see some tiny advancements in quality here. At this point I’d realized that my original comment color was nearly unreadable on the dark background, and brightened it up a bit.

But my voiceover still sounds kind of bored. And was still content to have long sections of silence while I put new code on the screen. These days, I try to always accompany coding with simultaneous explanation.

Read on for the original script and code…


In a previous episode, we looked at how the #fetch method on Hash can be used to assert that a given hash key is present.

auth = {
  'uid'  => 12345,
  'info' => {
  }
}

# ...

email_address = auth['info'].fetch('email')
# ~> -:11:in `fetch': key not found: "email" (KeyError)
# ~>    from -:11:in `<main>'

But what if the KeyError that Hash raises doesn’t provide enough context for a useful error message?

Along with the key to fetch, the #fetch method can also receive an optional block. This block is evaluated if, and only if, the key is not found.

Knowing this, we can pass a block to #fetch which raises a custom exception:

auth['uid'] # => 12345
auth['info'].fetch('email') do 
  raise "Invalid auth data (missing email)."\
        "See https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema"
end
email_address = auth['info'].fetch('email')
# ~> -:10:in `block in <main>': Invalid auth data (missing email).See https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema (RuntimeError)
# ~>    from -:8:in `fetch'
# ~>    from -:8:in `<main>'

Now when this code encounters an unexpectedly missing key, the raised exception will explain both the problem, and where to find more information.

The block argument to #fetch isn’t just for raising errors, however. If it doesn’t raise an exception, #fetch will return the result value of the block to the caller, meaning that #fetch is also very useful for providing default values. So, for instance, we can provide a default email address when none is specified.

email_address = auth['info'].fetch('email'){ '[email protected]' }
email_address # => "[email protected]"

Now, you may be wondering: what’s the difference between using #fetch for defaults, and using the || operator for default values? While these may seem equivalent at first, they actually behave in subtly, but importantly different ways. Let’s explore the differences.

Here’s an example of using the || operator for a default. This code receives an options hash, and uses the :logger key to find a logger object. If the key isn’t specified, it creates a default logger to $stdout. If the key is nil or false, it disables logging by substituting a NullLogger object.

This works fine when we give it an empty Hash.

require 'logger'

class NullLogger
  def method_missing(*); end
end

options = {}
logger = options[:logger] || Logger.new($stdout) 
unless logger
  logger = NullLogger.new
end
logger
# => #<Logger:0x000000030545a8
#     @default_formatter=
#      #<Logger::Formatter:0x00000003054580 @datetime_format=nil>,
#     @formatter=nil,
#     @level=0,
#     @logdev=
#      #<Logger::LogDevice:0x00000003054530
#       @dev=#<IO:<STDOUT>>,
#       @filename=nil,
#       @mutex=
#        #<Logger::LogDevice::LogDeviceMutex:0x00000003054508
#         @mon_count=0,
#         @mon_mutex=#<Mutex:0x000000030544b8>,
#         @mon_owner=nil>,
#       @shift_age=nil,
#       @shift_size=nil>,
#     @progname=nil>

But when we pass false as the value of :logger, we get a surprise:

options = {logger: false}
logger = options[:logger] || Logger.new($stdout) 
unless logger
  logger = NullLogger.new
end
logger
# => #<Logger:0x000000040bb608
#     @default_formatter=
#      #<Logger::Formatter:0x000000040bb5e0 @datetime_format=nil>,
#     @formatter=nil,
#     @level=0,
#     @logdev=
#      #<Logger::LogDevice:0x000000040bb590
#       @dev=#<IO:<STDOUT>>,
#       @filename=nil,
#       @mutex=
#        #<Logger::LogDevice::LogDeviceMutex:0x000000040bb568
#         @mon_count=0,
#         @mon_mutex=#<Mutex:0x000000040bb518>,
#         @mon_owner=nil>,
#       @shift_age=nil,
#       @shift_size=nil>,
#     @progname=nil>

That was supposed to be a NullLogger, not the default logger!

So what happened here? The problem with using || with a Hash for default values is that it can’t differentiate between a missing key, versus a key whose value is nil or false. Here’s some code to demonstrate:

{}[:foo] || :default             # => :default
{foo: nil}[:foo] || :default     # => :default
{foo: false}[:foo] || :default   # => :default

In contrast, #fetch only resorts to the default when the given key is actually missing:

{}.fetch(:foo){:default}             # => :default
{foo: nil}.fetch(:foo){:default}     # => nil
{foo: false}.fetch(:foo){:default}   # => false

When we switch to using #fetch in our logger-defaulting code, it works as intended.

options = {logger: false}
logger = options.fetch(:logger){Logger.new($stdout)}
unless logger
  logger = NullLogger.new
end
logger
# => #<NullLogger:0x00000003b73858>

When you want to provide default value for a missing hash key, consider carefully whether you want an explicitly supplied nil or false to be treated the same as a missing key. If not, use #fetch to provide the default value.

OK, that’s all for today. Happy hacking!

MVIMG_20190508_175746.jpg?resize=770%2C770&ssl=1

Let's correspond!

May I write to you? Every week or so you'll get a SIGAVDI newsletter with a few handpicked links to interesting articles, talks, or resources; an update on what I've published lately; and some reflections at the intersection of software and life. Replies go directly to me, and I try to respond to every single one (eventually!)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK