Announcing “Friend”: Adding Fine Grained Visibility Semantics to Ruby

By Loren Segal on April 04, 2010 at 41:212:76 PM

Note: this is not an April Fool's joke. And no, this notice is not an April Fool's joke either.

Update: 04/02/2010 – Thanks to Martin Luder who pointed out that you can in fact do visibility in Eiffel without the inheritance “export” syntax. Updated the example.

I’ve used Eiffel a couple of times. I’ve even blogged about it once. There’s one really cool feature in Eiffel that lives below the whole DbC radar. Peculiarly, the feature itself deals with “features”. Basically, Eiffel allows you to decide which classes can see which “features” (in Ruby features are basically methods) on an individual basis. Therefore, Eiffel has no need for a half-baked “public”, “protected”, “private” solution to the visibility problem because by exporting features you have extreme control over exactly how public, protected or private a feature can be. Here’s what it looks like:

feature {ANY} procedure_1 is do ... end
feature {NONE} procedure_2 is do ... end
feature {SOME_OTHER_CLASS} procedure_3 is do ... end

The first function is “exported” to ANY class (ANY is a supertype of all objects in Eiffel), the second to NONE, the last only to SOME_OTHER_CLASS. Note also that C++ also has this feature, but it has some limitations that make Eiffel’s implementation a little more useful (C++’s implementation does not respect inheritance, for instance).

I’ve always wanted to see this kind of fine-grained visibility implemented in other languages, and last night I managed to implement a neat little gem for Ruby called “friend” (taken from the C++ idiom rather than Eiffel’s) which lets you do just that.

A Quick Example

Here’s a quick example of how the gem works. You create your methods and then “export” them to specific classes (and their subclasses). Doing this allows only those classes to access the method:

require 'friend'

class A; def bar; D.new.foo end end
class B; def bar; D.new.foo end end
class C; def bar; D.new.foo end end

class D
  def foo; "HELLO WORLD!" end

  # Export to A and B, but not C
  export :foo, A, B 
end

puts A.new.bar
puts B.new.bar
puts C.new.bar

# Output:
# 
# HELLO WORLD!
# HELLO WORLD!
# export_features.rb:5:in `bar': `foo' is not accessible to C:Class (NoMethodError)
#   from export_features.rb.rb:16:in `<main>'

Note that we could export :foo to Object and then every class could access this method.

Why Do I Need Fine-Grained Visibility?

Glad you asked.

Many large systems end up with at least a few “semi-private” methods that are used only internally to communicate between classes but are not meant for the user to see or use. In other words, they are only meant to be called by specific components. This becomes hard to document, because these methods are “public” in Ruby’s eyes but not exactly “public” in the eyes of your documentation tool. This leads people to abuse RDoc’s :nodoc: directive to hide methods arbitrarily, which in turn inspires people not to document these methods (even for internal use). In fact, this case was the inspiration to add “@private” to YARD (after some serious debate with many people) and is the one case where it is valid to use this tag on a method (rather than declaring it private). Of course, both of these solutions are hacks. RDoc’s solution implies that the method needs no documentation when it might in fact require quite a lot of developer-only docs (developers need docs too). YARD’s solution implies that the method is private, but it really is not (any class could call the method if they discovered it in private docs— even without send()). Neither of these solutions expresses the true nature of the method’s visibility.

But Ruby is about expressing yourself in code, not in documentation, so why not allow the programmer to express this semantic directly? This is both cleaner and shows direct intent without resorting to documentation. Friend lets you do this quite explicitly.

Another Example: The C++ “friend” Idiom

We talked about “semi-private” methods, or “private interfaces”, but our first example didn’t really show it. The C++ “friend” idiom illustrates this best, so let’s see how we would write one of these private interfaces using the Friend gem. For the example we’ll look at the interface of a Car, where only the “Car” class itself can turn on the engine using a key, disallowing the user from engaging the Engine directly:

class Car
  attr_reader :engine
  def initialize; @engine = Engine.new end
  
  def turn_on(key)
    if key == "foo"
      @engine.engage
      puts "Car turned on!"
    else
      puts "Wrong key!"
    end
  end
end

class Engine
  private
  def engage; puts "Engine turned on!" end
  friend :engage, Car
end

# We can enable the engine if we 
# turn the car on with the right key
car = Car.new
car.turn_on("foo")

# But we can't enable the engine directly
car.engine.engage

# Output:
# Engine turned on!
# Car turned on!
# export.rb:17:in `block in export': `engage' is not accessible outside 
# Engine (NoMethodError)
# 	from friends.rb:29:in `<main>'

In the example, car.turn_on will work fine (so long as we have the right key), but calling car.engine.engage directly on the engine (are you trying to hotwire it?) will raise a NoMethodError exception, citing the method is inaccessible to us.

But not only do we get this cool runtime protection, we also get our method properly documented as “private” by just about every tool you would use. In addition, you could easily write a YARD plugin to handle this “friend” semantic, explicitly declaring which classes this method is visible to. Here’s a mockup of what it could look like:

YARD friend plugin mockup

The Friend Gem

You can install the gem with a simple gem install friend. Note that it does need to build a native extension since it requires some digging into the Ruby VM/interpreter to grab the calling class (Kernel#caller is oddly missing this information). You can actually implement it with set_trace_func, but that would be super slow.

As usual, the source is on Github: http://github.com/lsegal/friend

Feel free to poke around and improve the project and let me know if you use it.

Questions? Comments? Follow me on Twitter (@lsegal) or email me.