Ruby’s private
keyword means something different
compared to basically all other object-oriented languages. Most other
languages don’t even have a feature matching what Ruby calls
private
, but incredibly, Scala does, which it calls
protected[this]
(meaning “object-protected”, as opposed to
the normal protected
keyword which is called
“class-protected”).
First let’s review what private
normally means, and then
discuss what private
in Ruby means (which will also amount
to an explanation of what protected[this]
means in
Scala).
Background: the normal
meaning of private
Conventionally, private
means “only code contained
inside my class’s body can access this member,” for example in Java:
class Parent {
private int x;
Parent(int x) {
this.x = x;
// └── (1) allowed ✅
}
boolean equals(Parent other) {
return this.x == other.x;
// │ └── (2) allowed ✅
// └── (1) allowed ✅
}
}
class Child extends Parent {
Child(int x) { super(x); }
int example() {
return this.x; // (3) not allowed ⛔️
}
}
Parent parent = new Parent(0);
parent.x; // (4) not allowed ⛔️
In this example we see a member x
marked private. In
summary:
- ✅ The member
x
can be accessed asthis.x
inside methods within the class body ofParent
- ✅ The member
x
can also be accessed on other instances ofParent
within the class body ofParent
, likeother.x
- ⛔️ The member
x
cannot be accessed in subclasses ofParent
, likeChild
- ⛔️ The member
x
cannot be accessed outside of the inheritance hierarchy ofParent
Now let’s translate this example to Ruby to see what restrictions the
private
keyword in Ruby brings.
Restrictions on
private
in Ruby
Here’s the same Java example, converted to Ruby. What we see is that points (2) and (3) flip!
class Parent
attr_accessor :x
private :x, :x=
def initialize(x)
self.x = x
# └── (1) allowed ✅
end
def ==(other)
self.x == other.x
# │ └── (2) not allowed ⛔️
# └── (1) allowed ✅
end
end
class Child < Parent
def example
self.x # (3) allowed ✅
end
end
parent = Parent.new(0)
parent.x # (4) not allowed ⛔️
Here’s what Ruby’s private
keyword allows:
- ✅ The member
x
can be accessed asself.x
inside methods within the class body ofParent
- ⛔️ The member
x
cannot be accessed on other instances ofParent
within the class body ofParent
, likeother.x
- ✅ The member
x
can be accessed in subclasses ofParent
, likeChild
- ⛔️ The member
x
cannot be accessed outside of the inheritance hierarchy ofParent
Why is Ruby like this?
Most other object-oriented languages rely on type checking to report visibility errors before the program runs. Ruby doesn’t have a type checker and also is quite dynamic. Classes can be reopened and extended basically at whim, and the notion of “within the class body” barely exists in Ruby because of how easy it is to dynamically define methods from anywhere:
module MyDSL
def make_method
:get_x) do
define_method(MyDSL.internal_helper
# └── should this be allowed? 🤔
# (technically inside the class body of MyDSL)
self.x
# └── should this be allowed? 🤔
# (technically not inside the class body of Parent)
end
end
private_class_method def self.internal_helper
# ...
end
end
class Parent
extend MyDSL
make_methodend
Real-world Ruby code tends to toss away conventional notions of “defined inside the class body,” and even if it didn’t, it wouldn’t have a type checker to easily check visibility.
Instead, Ruby picks a simpler way to enforce visibility: a private
call is allowed anywhere that the receiver is either omitted or is
self
, syntactically.The receiver is the x
in
x.foo
. When a method call’s receiver is omitted like
foo()
, Ruby implicitly assumes that it had been called like
self.foo()
.
That syntactic restriction means that things like this
are not allowed:
class A
private def foo; end
end
def identity(self); self; end
self).foo # ⛔️ not syntactically `self.foo`
identity(self.itself.foo # ⛔️ not syntactically `self.foo`
By using local syntax to determine when it’s okay to call private
methods, Ruby ends up allowing access via inherited classes and denying
access via something like other.x
. This mechanism is very
simple to check: it just a matter of remembering a bit like
isPrivateOk
per method call and a bit per method def like
isPrivate
, which can be done without any sort of
non-local/static analysis.
Ruby is not unique:
protected[this]
in Scala
For a while, I thought that Ruby was unique in having a visibility
modifier that worked like this, but recently I learned that Scala
actually has a similar feature: protected[this]
. The name
wasn’t immediately obvious to me, but it’s actually kind of
sensible:
- Like the
protected
visibility modifier,protected[this]
allows (certain kinds of) member access from subclasses. - The
[this]
portion is called an “access qualifier” which limits all access to happen via thethis
keyword.
It seems that Scala allows other things to appear inside the
[...]
, but I stopped short of wrapping my head around what.
I learned about this feature from this page:
→ Scala Language Specification, Version 2.13, Chapter 5 Classes and Objects, Section 5.2 Modifiers
Maybe worth a skim if you’re more curious than I was.
Scala also has private[this]
, which excludes access via
subclasses (leaving only access via this
inside the parent
class). Ruby doesn’t have a visibility level matching this, which means
that Ruby has no means to hide a member from subclasses.
A note about Ruby instance variables
Despite not being declared with the private
keyword,
instance variables in Ruby behave exactly like private
methods!
class Parent
def initialize(x)
@x = x
#└── "declares" an instance variable
# (automatically private)
end
def ==(other)
@x == other.@x
#│ └── (2) syntax error ⛔️
#└── (1) allowed ✅
end
end
class Child < Parent
def example
@x # (3) allowed ✅
end
end
parent = Parent.new(0)
parent.@x # (4) syntax error ⛔️
So you might next ask, “would it make sense to have ‘public’ instance variables?” I can see arguments both ways:
Pros
- Maybe it scratches a burning itch for symmetry?
- It could be used to resolve problems that arise in Sorbet’s control
flow-sensitive typing, which I discussed
in a previous post.
- Counter point, also raised in the post above: you could imagine
solving this another way, by treating
x.foo
andx.foo()
differently (despite both meaning the same thing in the Ruby VM).
- Counter point, also raised in the post above: you could imagine
solving this another way, by treating
- Maybe (maybe) it could be optimized more easily by the Ruby
VM?
- Counter point, the VM already has a lot of special cases for
attr_reader
-defined methods, so I’m not actually certain whether this point is valid.
- Counter point, the VM already has a lot of special cases for
Cons
- In order to keep backwards compatibility, it wouldn’t even be all
that symmetric.
- Methods would default to public visibility and have a
private
keyword, but instance variables would default to private and have some sort ofpublic
keyword.
- Methods would default to public visibility and have a
- Instance variables in Ruby represent encapsulated state. They shouldn’t leak into the public API, and should instead be exposed by proper methods.
It’s not a fight I want to start (nor one I feel strongly about). But I will at least point out that nearly all other languages expose a (syntactic) difference between calling a method and accessing an attribute.
Any other languages?
So far, I’m only aware of Ruby’s private
modifier and
Scala’s protected[this]
which behave like this. If you know
of any other languages, please email me! I’d love to hear about
them.
Appendix: Some unanswered questions
Question 1: why does Scala have both
private
/protected
andprivate[this]
/protected[this]
?I don’t know of the history there, but I do know that the latter is useful especially in generic classes with covariant and/or contravariant type members. More on that in another post (maybe). I will say though, it’s actually quite lucky that Ruby works the way it does, or Sorbet would not be able to provide generic, co-/contravariant classes with as few changes as this!
Question 2: what’s up with Ruby’s
protected
keyword?A great question, but one that I’ll have to save for another post!