Better Know A Ruby Thing -- Methods and Access Control (part 1)
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:
- Programming Ruby 3.3 (The Pickaxe Book) is available in ebook from Pragmatic Press and physically from Amazon among others. I’d love it if you purchased a copy. Prag has it on sale through June 5 with coupon code "2024Redux".
- If you’d like to support this newsletter, you can sign up for a Monthly Subscription for $3/month or an Annual Subscription for $30/year. Subscription money goes toward paying for Buttondown. Paid subscribers just got access to the video I did for Graceful.Dev, and there are a few other things coming.
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 thedef
when it is executed (using aclass << 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 treatsprivate
as a block, which it isn't. If you useprivate
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!
Dynamic Ruby is brought to you by Noel Rappin.
Comments and archive at noelrappin.com, or contact me at noelrap@ruby.social on Mastodon or @noelrappin.com on Bluesky.
To support this newsletter, subscribe by following one of these two links: