[go: up one dir, main page]

Noel Rappin Writes Here

Better Know A Ruby Thing: Methods and Access Control (part 1)

Posted on May 29, 2024


I’ll be honest, I picked this topic out of the half-dozen or so Better Know A Ruby Things on my to-do list strictly because it’s maybe the only Ruby take that I genuinely argue with people about. To be even more honest, it got away from me a bit as I started writing the argument: which is why I tend to avoid declaring methods private.

I know these newsletters have tended toward long, but 3100+ words was a bit much even for me, so I’ve split it in half. Today, we’re covering methods, method definition, and access control in Ruby. The bit about private methods will be coming a a few days.


Time for the commercial:

Thanks!


Starting Here: What’s A Method?

Most programming languages have some way of gathering common behavior so that it can be invoked together and also given a useful name. Typically, this is done so you can reuse the behavior, and the name often makes the intent of the code easier to understand.

The term for a gathered bundle of code is different in different programming languages. Here’s a quick tour:

Procedure: (sometimes “subprocedure”) is the most generic term, any bundle of code could be referred to as a procedure, but the term is most often used in non-object-oriented imperative languages like Pascal. In Ruby, this term survives as the base term that the class name Proc is derived from.

Technically, a function is any procedure that returns a value. That’s how, say, Pascal used it, though many languages use “function” and “procedure” roughly as synonyms. Ruby doesn’t officially call things functions, but Ruby developers will call code “functional” if it doesn’t use mutable state and instead tends to chain together methods.

A method is a procedure that is attached to an object in an Object-Oriented language. It seems like the first language to use that term was Simula, which was, not coincidentally, the first OO language. Simula also introduced the dot syntax (receiver.method) for calling methods. I couldn’t find a description of why Simula picked up this term, but I think it’s based on a technical use of the word in math, or possibly short for “methodology”. “Method” is the official term in Ruby for “the thing that is part of an object’s functionality”.

Message is a word often applied to calling a method in an OO language, so you “pass a message” to an object which responds by “invoking a method”. This is largely a Smalltalk thing, because Alan Kay really likes the idea of objects passing notes to each other in class. We also use this in Ruby, at least unofficially (the Pickaxe book uses it, though I don’t think the official docs do).

Creating Methods In Ruby

In Ruby, you create a method with the keyword def – since it’s a keyword, the behavior of def can not be overridden. (Fun fact: per the official docs, Ruby defines 43 keywords, if I am counting correctly, plus either 7 or 20 symbol patterns that can’t be overridden as operators – depending on if you count all the assignment variants like += separately…)

Anyway, the syntax is

def <method_name><optional method arguments>
  # arbitrary ruby code
end

If the arbitrary Ruby code is exactly one expression, then you can use the “endless” method definition form instead:

def <method_name><optional args> = <that one expression>

A couple of things worth pointing out:

  • This is usually clear from context, but def adds the newly created method to the innermost class or module surrounding the def when it is executed (using a class << self counts as a class for this purpose).
  • If you define a method at the top level, either in a script, or in irb, it is part of a special object main and is set up in a way that allows you to also call the method from the top level.
  • The def declaration returns a value – the name of the newly created method as a symbol, which you can confirm in irb. This turns out to be useful, as we’ll see in a moment.

There’s one other little twist to the syntax, which is that the method name can be prefixed with an object, as in def foo.first_name, in which case the method is attached to the specific object used as the prefix. We’ll talk about this particular quirk more in a future Better Know.

Access Control

A key question in any Object-Oriented language is “who gets to use my stuff?”.

One of the basic ideas in OO programming is that an object has an interface, which is public and represents what the outside world knows about the object, and a potentially separate implementation, which private and is how the object does those things when nobody is looking. The language defines access control, which is a way to allow the object to specify that some methods are part of the public interface, while others are part of the private implementation.

Several OO languages have this public/private distinction built directly into the language. Java has public, a strict private, and protected, and a default access that is, almost public? I think? It’s been a while and I don’t remember. C# has, according to this doc page I just looked up, seven different access modifiers. Which, frankly, seems like too many.

Other OO languages don’t build the distinction in to the language. In Smalltalk, all methods are public (and all instance variables are private), but there’s a convention that if a method starts with an _, you shouldn’t call it externally. Python has a similar convention, but does actually handle methods that start with a double underscore __ differently so as to make them hard to call from outside the class.

How is Ruby Different?

Ruby, perhaps not surprisingly, takes kind of a middle path in that it offers both access control and the means to get around access control. You might reasonably argue that there’s no point to access control if it can be circumvented, but part of the point is just to make it kind of annoying to get around it, so you know that you are doing something that is not necessarily a good idea.

A method in Ruby is either public, protected, or private. We’re not going to talk about protected here – the Ruby docs say not to use it, and the use case is super-rare. I’ve been using Ruby for nearly 20 years and I’ve never even considered declaring a method protected, so in the name of keeping this discussion simple, we’re just going to mention that protected exists, and wave to it on the side of the road as we drive by.

Ruby access control is based on the identity of the object receiving the method. Remember that in Ruby, there is always a receiver to a method, as in foo.upcase, but if the receiver is not explicitly defined, as in something like puts "foo", then the receiver is implicitly set to self for whatever value of self is current.

The default access is public. A public method can be called anywhere, with any receiver, implicit or explicit, at any point in the code.

A private method can only be called with self as the receiver, but that self can be implicit or explicit. Typically, this means that a private method can only be called from within the class in which it is declared, because that’s the only time that self will be set to an instance of that class. (You’ll still see Ruby guides say that private only works with an implicit receiver, but the behavior was changed in Ruby 2.7 to allow explicit self.some_private_method calls. It turned out that there were some edge cases that were easier to manage if the explicit self was allowed in private calls).

But… this is Ruby, and there’s always another way. You can still access private methods using send as in x.send(:some_private_method), so nothing is ever really private.

You switch from private to public with the methods private and public, which are both instance methods of the class Module. Both methods have no-argument and with-argument versions.

When you call the no-argument version, it changes the default access for methods that are defined subsequently in the file, so:

class FictionalThing
  def method
    "this is public"
  end

  private

  def another_method
    "this one is private"
  end

  public

  def one_more
    "this is back to public"
  end
end

It’s worth stressing that public and private are methods and not keywords – though the behavior of changing the default for subsequent methods is designed to make it easy to think of them as keywords. Ruby loves having things that are Object-Oriented that don’t look Object-Oriented.

The argument version of both methods takes a string, a symbol, or a list or array of strings or symbols. The methods associated with the arguments are changed to that access mode. When these methods are called with an argument, the access for other methods defined later in the class is not changed, only the access for the methods whose names are passed as arguments.

class MadeUpLogic
  def a_method
    "right now this is public"
  end

  private :a_method
  # at this point, a_method is private

  def another_method
    "this method is still public"
  end

  # until now
  private :another_method
end

Toward the beginning of this post, I mentioned that def returns the symbol name of the method. And now we see that private and public can take the symbol name of a method and change the access level of the method.

Combining those two facts, we can put private in front of def:

class Arbitrary
  private def another_method
    "this method is now private"
  end
end

The way to read this is private(def method ...). In other words, the entire def is executed, and the returned value – the name of the method as a symbol – is the argument to private. Therefore, the method is immediately marked as private.

You might be wondering what value the private and public methods return. If called with one or more names as arguments, those methods return the same names, allowing private to be chained with other methods that are designed to be used before def statements. If called with no arguments, they just return nil.

Which leads me to:

My lesser hot take: use private as a prefix

The ability to write private def is relatively new in Ruby, and I don’t think the syntax is fully appreciated.

It’s a common pattern for a class to end with multiple private methods, enough that the actual private method call goes off the top of the screen as you look at them. The number of times I have added a method to the bottom of the class expecting it to be public only to have it be private because of a declaration that I didn’t see – well, it’s more than zero.

Putting the private call in front of the method rather than above all the private methods has the following advantages:

  • It’s more explicit – you can see right at the point of the method declaration if the method is private.
  • It’s more flexible – you can have your private methods in any order. For example, you can have private methods near the public methods that call them.
  • It’s easier to explain – I think the concept of “all methods after this call have a different default” is a little tricky to explain.
  • It’s more consistent – some people (like, the Rails core team) indent methods after the private call, which treats private as a block, which it isn’t. If you use private as a prefix, then you don’t have to worry about that spacing.

The only downside, I think, is that you have a little more typing. If you have a lot of private methods.

Which leads me to my hotter hot take about private methods, but we’ll get to that one in a few days…

Thanks for reading!



Comments

Please enable JavaScript to view the comments powered by Disqus. comments powered by Disqus