In Progress
Unit 1, Lesson 1
In Progress

Safe Navigation Part 4: Refinements

Video transcript & code

In the preceding episode, we patched the core classes Object and NilClass so that they would behave like optional types in functional programming languages. We've made it possible to safely chain operations together on singular values in exactly the same way that we are used to on collections.

One reasonable objection you might make to this code is that adding #map and #each to Object and NilClass are some pretty big changes to make to core classes. It's always possible this kind of invasive patching might break existing code.

Which makes these extensions a natural candidate for translation into refinements. I know we've talked about refinements before, in episode #250. But they are such a useful feature for trying out extensions in a safe way that I think it's worth going over the how-to one more time.

To make our patches into refinements, we first enclose them in a module. Then we change the class declarations into refine...do statements.

When that's done, we just need to add a using statement to tell Ruby we want to import the refinements into the current scope.

module OptionalEverything
  refine Object do
    def each
      yield self
      nil
    end

    def map
      yield self
    end
  end

  refine NilClass do
    def each
    end

    def map
    end
  end
end
require "./models2"
require "active_support"
require "active_support/core_ext"
require "./refinements"

using OptionalEverything

@product.departments[1].curator = nil
@product.map(&:departments).map(&:curator).compact.map(&:email_address).each do |email|
  puts "Emailing a complaint to #{email}..."
end

# >> Emailing a complaint to dan@example.com...

And that's it. Now our changes are in effect only in this file, and will not "leak out" to infect and potentially break other code.

Of course, we would probably want to put these refinements into a file of their own. We do this the same way we would for any module. We just move it into its own file, and then require the file where we want to use the refinements.

So now we have a way to perform so-called "safe navigation" across networks of optional object relationships. It uses methods that are familiar from operating on collections. It is more robust than #try or the upcoming Ruby "safe navigation operator" in the face of changing relationship cardinalities. It doesn't involve new language syntax. And now thanks to refinements, we can safely use this approach where we want it, without impacting any other code.

Happy hacking!

Responses