Github user supersam654 recently opened a great issue on the Flipper repo after running into trouble trying to register a group using Ruby’s &:method shorthand.

issue

Groups allow enabling features based on the return value of a block, which is passed the actor when checking for enabledness.

&:admin?

When Rubyists want to call a method on every member of an Enumerable we tend to avoid the extra keystrokes needed to type out the whole block:

["a", "b", "c"].map { |letter| letter.upcase }

#=> ["A", "B", "C"]

in favor of the more concise:

["a", "b", "c"].map(&:upcase)

#=> ["A", "B", "C"]

This works with any Enumerable method that yields each member of the collection to the provided block. If this works with map, each, select, etc. why is supersam654 having trouble using it with Flipper.register? To answer this we’re going to have to take a tour of some of the more advanced features of Ruby. Let’s get started.

Registering Groups

When a Flipper group is registered it stores the provided block in an instance variable @block.

Given a simplified version of register:

source

def register(name, &block)
  @block = block
end

When this code is executed:

Flipper.register(:working) do |actor|
  actor.admin?
end

We end up with a gate instance that has a match? method locked and loaded, ready to check if a feature is enabled for an actor:

 def register(name, &block)
   @block = block # { |actor| actor.admin? }
 end

def match?(thing, context)
  if @block.arity == 1 # @block = { |actor| actor.admin? }
    @block.call(thing)
  else
    @block.call(thing, context)
  end
end

When checking if a feature is enabled for a given user, match? eventually gets called with the user and optionally the context if your provided block takes two arguments . The feature is enabled for the user if the result of the block is true.

Arity

if @block.arity == 1 # @block = { |actor| actor.admin? }
  @block.call(thing)
else
  @block.call(thing, context)
end

Arity is the number of arguments a function takes (Thanks Wikipedia!, and screw you high school teacher who said Wikipedia was not a credible source for my essays!).

Ruby, with its powerful metaprogramming abilities lets us introspect any object we’d like including method objects. Who said Ruby methods aren’t first class citizens? To figure out a method’s arity we don’t need any special wizardry we just need to politely ask it.

def greet(person)
  "Nice to meet ya #{person.name}"
end

method(:greet).arity
# => 1

def introduce(person1, person2)
  "#{person1.name}, I'd like to introduce you to #{person2.name}"
end

method(:introduce).arity
# => 2

When a method takes a variable number of arguments arity returns -n-1, where n is the number of required arguments.

def call(*people)
  people.each(&:call)
end

method(:call).arity
# => -1

Just like methods, procs and lambdas can also be asked their arity.

proc { |arg| }.arity
# => 1

->(arg) { }.arity
# => 1

lambda {|*args| }.arity
# => -1

Lies, Lies, and More Lies

I have to apologize for telling a slight lie earlier. The register method doesn’t just store the block in an instance variable @block. When & is prepended to a parameter like &block Ruby converts the provided block to a proc, which allows us to reference it by dropping the ampersand. You can name this whatever you want e.g. &foo. If passed an argument prepended with & instead of a block, it converts the argument to a proc by calling to_proc on it.

def register(name, &block)
  # Before giving us a refernce to block
  # Ruby implicitly calls: block = block.to_proc
  @block = block
end

When register is passed:

register(:admin, &:admin?)

The following happens:

def register(name, &:admin?)
  # block = :admin.to_proc  - Ruby calls this under the hood for us
  @block = block
end

And what would happen if we checked this proc’s arity?

def register(name, &block)
  # block = :admin.to_proc  - Ruby calls this under the hood for us
  @block = block
  puts @block.arity
end
register(:admin, &:admin?)
# => -1

register(:admin) { |actor| actor.admin? }
# => 1

Hold up. With the block the correct arity is returned, but with &:admin? -1 is returned! This explains why Flipper.register is working with blocks, but not the &:admin? shortcut.

def match?(thing, context)
  if @block.arity == 1
    @block.call(thing) # This path gets called with block
  else
    @block.call(thing, context) # This path gets called  with &:admin and raises an exception
  end
end

To figure out what exactly is happening here we’ll need to keep on digging.

I thought the following did the exact same thing…

["a", "b", "c"].map { |letter| letter.upcase }
["a", "b", "c"].map(&:upcase)

Almost, but not quite. This is because Symbol implements its own version of to_proc.

simplified version of Symbol#to_proc. The real implementation is now written in C

class Symbol
  def to_proc
    proc { |arg| arg.send(self) }
  end
end

If we had a symbol such as :upcase, when invoked:

["a", "b", "c"].map { |letter| letter.upcase }
["a", "b", "c"].map(&:upcase) # same as the shorthand

we could replace these variables to see what it would actually look like:

def to_proc
  proc { |arg| arg.send(self)
# proc { |letter| letter.send(:upcase) }
end

Now we understand what this &:upcase business is all about, but that still doesn’t explain why the two examples above aren’t actually the same. That’s beacuse I’ve lied done it again! Another lie! The real definition of Symbol#to_proc looks more like this:

class Symbol
  def to_proc
    proc do |*args|
      receiver, *rest = args
      if rest.nil?
        receiver.send(self)
      else
        receiver.send(self, *rest)
     end
    end
  end
end

As seen in our simplified example above Symbol#to_proc always returns a proc that takes a variable number of arguments, while blocks converted to procs via &block return procs that take the exact same number of arguments as the block:

def arity(&block)
  block.arity
end

arity() { |user| user.admin? }
# => 1

arity(&:to_s)
# => -1

This means that in the &:admin? case we end up in the else condition and pass arguments to admin?, which expects 0. As always the computer does exactly what we tell it to do and not what we want it to. The computer wins and an exception is raised.

def match?(thing, context)
  if @block.arity == 1 # @block = { |actor| actor.admin? }
    @block.call(thing)
  else
    @block.call(thing, context) # this path gets called
  end
end

Ruby friends to the rescue

Tim, being the legend that he is opened a PR using Sam’s (also total legend - both of whom I’ve never actually met, but I’m sure they’re legends) proposed idea to take the absolute value of the arity in match.

arity

pr

Since this has been merged it should be going out in the next release after 0.16.2. As always check out the Changelog to be sure.

Follow Up

If you thought this was a fun dive into some parts of Ruby you may not have explored I encourage you to experiment and implement your own to_proc method. Think of some ways you can make it more powerful. I’ll get you started:

class String
  def to_proc
    proc { |arg| arg.send(self) }
  end
end

["a", "b", "c"].map(&"upcase")