Hacker News new | past | comments | ask | show | jobs | submit login
Ruby's private keyword is weird (jez.io)
109 points by jez 10 months ago | hide | past | favorite | 38 comments



The other approach to private methods is weird.

It breaks the object-oriented paradigm. Why should my Dog be able to check out the internal StomachContents of another Dog, while that information isn't available to a Vet or even to the Dog's Owner?

Possibly the Dog can even check out the StomachContents of a Wolf or an Elephant, depending on your language and your inheritance rules. Even if the Dog otherwise has no relation to the second animal.

It's because we're seeing behind the curtain. The internal implementation of method Fido.vomit() is some equivalent of Dog::vomit(Fido) so the current object isn't actually in as privileged a position as it might appear to a reader. The language privileges all members of class Dog instead.

Ruby has the same problem even more, because the first thing any Ruby library author does is to monkeypatch everyone else's objects. It solves it in the opposite way, by really trying to make the language know when an object is calling a method on itself, but again the implementation leaks and the language runtime can't always know that. Which is fine - Ruby's philosophy is to provide convenient escape hatches for this kind of thing anyway, and to limit you by convention rather than by compiler errors.


It's a great example and I appreciate you writing it out, but Dog.stomacheContents... you must be programming dwarf fortress!


> the first thing any Ruby library author does is to monkeypatch everyone else's objects

Serious Ruby libraries don't do that very often


Visibility rules are not for objects. These exist for defining class implementation / maintenance borders. Iow, for gods of all dogs and elephants to not step on each other feet. Objects themselves have nothing to do with that and OOP-world analogy was always shallow to begin with, at least in mainstream OOP.


> OOP-world analogy was always shallow to begin with

I would also add that the purpose of visibility is to encapsulate and not some sort of access control. All the objects of the same class are implemented with the same code so they are within the same encapsulation boundary.


What do you mean by access control? I don't know if this falls under the umbrella of encapsulation as well, but often in Rust I'll mark members of a data structure private so that it can't be constructed except by approved functions exported by the same module that contains the data structures. As long as team boundaries are enforced at the repo level, this becomes great access control.


I think this is in response to

> Why should my Dog be able to check out the internal StomachContents of another Dog

This seems to be interpreting the purpose of visibility as controlling the access that actual things represented by objects have. (There could be a non oop-as-real-world-things example here; `UserProfile.equals(UserProfile other)` might access secrets of `UserProfile` that the different actual users are not supposed to see between each other.)

The reason Dog can check the internal details of another Dog is, there's a single file that controls the usage of the fields. If you decide you want to change the semantics of the stomachContents field, you can check all the usages in that file. That's the simple practical reason. The StomachContents thing seems like a confusing analogy, like most that compare OOP to Real World Categorization.


The point is not about if schnauzerA instance can access stomach contents of dachshundB instance but about the classes Schnauzer and Dachshund.

Any dog instance would be inheriting and refering to the same implementation and the class has an inherent link to it. Contrast this to other classes, which may or may not ultimately be interfacing towards the exact implementation available at the time.

One practical aspect: If some private function is changed or removed in a patch release and not mentioned in release notes and your code using it breaks, that's entirely on you. It's not that you aren't allowed to futz around with it, it's that it's not part of the contract. If you hook into class internals and stuff break in an unexpected way when you upgraded and didn't manually go over the code diff, that's entirely on you.

This is different from, say, guaranteeing that data never crosses boundaries or otherwise as a security measure. It's about separation of concerns, not security.

> As long as team boundaries are enforced at the repo level, this becomes great access control.

If you're talking about an an internal codebase where this is strictly enforced, I'd argue that those measures are what would constitute access control. Fundamentally it's not much different from a library author proclaiming "users may only be calling functions starting with letters A-H". Does putting linting rules in place for that turn it into access control?

What you're actually talking about is sounding more like object capabilities (ocaps).


I think the parent was saying that private/protected aren't used to protect secrets, they are there to make code more maintainable when you have a split between implementation details and stable/revisioned public api "contract".

You want to understand exactly what code may be depending upon implementation details and limit it (encapsulation) for reasoning and maintainability.

I'll give a counterexample of this being done wrongly - Java has a pretty common development pattern where up-front you make a member private, and then write pass-through getter and setter methods. This doesn't really provide much for maintainability, because you have started out needing to support arbitrary retrieval and modification from anywhere, and have likely externalized interpretation of the data the member represents.


Smalltalk works this way: an object's instance variables can only be accessed directly in methods called on that object (and not in methods defined in subclasses either). And method calls aren't magical function calls, if mostly because methods are the only way of executing code: you do have blocks, but they're invoked by calling a method on them, so...


> And method calls aren't magical function calls

Precisely, unlike C++/Java/Python/Go, Ruby and Smalltalk methods are invoked through message passing†; ObjC as well.

Superficially it looks the same but the farther you go into the rabbit hole, the more you see each paradigm has deep consequences that end up surfacing here or there when you compare one language to the other.

> you do have blocks, but they're invoked by calling a method on them

Again, same in Ruby: an implicit block can only be called directly by `yield` inside the binding the block is internally tied to, or if it has been reified into a Proc, to which one sends the `call` message.

† Hence `respond_to?` and `send` in Ruby, often used to circumvent privacy checks done by syntactic `.method` invocation. This is also what enables invoking methods that don't actually exist with `method_missing` (to be paired with `respond_to_missing?`).


I’ve only glanced quickly at the article, but I think the author’s understanding of private in Ruby is wrong.

    class Parent
      attr_accessor :x
      private :x

      def initialize(x)
        self.x = x
        #    └── (1) allowed 
      end

      def ==(other)
        self.x == other.x
        #    │         └── (2) not allowed 
        #    └── (1) allowed 
      end
    end
This is not doing what the author thinks it is doing. Or, it is, but I don’t think the authors understand why. `private :x` only marks the x method as private, but `x=` is a different method.

They would see different behavior with

    private :x
    private :x=
In general, public, private, and protected do the following. Public methods can be called with any receiver: explicit (x.foo()), self (self.foo()), or implicit (foo()). Protected methods can be invoked on self or with an implicit receiver. Private methods can only be invoked on an implicit receiver. Note that this has nothing to do with the class hierarchy. A private method can be called from a subclass as long as it has an implicit receiver.

The difference between protected and private is not really practically useful, so I personally use them to signal different levels of “private-ness”. I use protected methods to implement a class’ “internal API”, which is the core thing the public API is abstracting and exposing. I use private methods for implementing ancillary details that aren’t really core to what the class does but handle some one-off unrelated data manipulation or munging. But that’s just me.

I’ll also admit that something may have changed with Ruby’s rules around these keywords around 3.0 that I missed, so if that’s the case I apologize.


Sorry, that’s simply a typo translating from Java to Ruby. I believe the point that section is trying to make still holds after editing it to say “private :x, :x=”

> Private methods can only be invoked on an implicit receiver.

This changed in Ruby 2.7. You can now include the self keyword, which is important because it allows calling methods that share names with Ruby keywords. “begin” is a keyword but “self.begin” is a call to a method named begin. So if that method happens to be private, you can call it now in Ruby 2.7 without needing “send(:begin)”

Other than the typo (thanks! I’ll fix that when I’m back at my computer), I think that the rest of the post underscores what you’ve already pointed out about how private works, modulo the misunderstanding about the changes in Ruby 2.7


Along these lines, I have a nit to pick: "private" isn't a keyword in Ruby. It's just a method on Module.


Aha! Glad I caveated things by saying I may have missed a change in semantics. I’m surprised they did that in a minor release and not a major one.

So, then, is there no actual difference between private and protected now?


The difference between private and protected is a bit subtle so I’ll defer to the Ruby FAQ:

https://www.ruby-lang.org/en/documentation/faq/7/

(On this page there’s a section called “What’s the difference between private and protected?” which explains it.)

It’s interesting in its own right, and it’s in my backlog to blog about because I don’t like any of the existing resources contrasting the two.


From the examples in this FAQ, it seems like protected works a bit more like Java's private - you can use it only inside the class, but for any object of that class, not just self, right? (with the exception that it applies to subclasses)

So Ruby's private only allows you to interact with other methods on the current object. Protected allows objects of the same class to send each other the message, but not other classes. Would this be a fair summary?


Ruby private literally does nothing. It's a marker with some side effects, but isn't actually protecting anything. You can trivially do obj.send(:private_method) for any method, private or not.

It's been about a year since I last looked at Ruby code (a true fave, holds it down, easy to love), but I'm pretty sure that's the idea.

In Ruby, the private keyword is more about controlling the visibility of methods rather than providing strict access control. It's a part of Ruby's philosophy of "convention over enforcement." While it does mark methods as private, it doesn't prevent you from calling them using .send. This is a design choice in Ruby to prioritize flexibility and programmer discretion.

Here's a bit more detail on how it works:

Method Visibility: When you declare a method as private, you're essentially saying that it should only be used within the class where it's defined (or its subclasses). This is a way to indicate the intended usage of a method.

No Strict Access Control: Unlike some other languages, Ruby doesn't enforce strict access control. It trusts the developer to follow conventions. However, this doesn't mean you should casually break encapsulation. It's a matter of design and discipline.

send and public_send: The send method allows you to invoke any method, including private ones, on an object. It's a powerful feature that can be handy in some situations but should be used with caution. There's also public_send, which respects the visibility rules and won't invoke private methods.

Here's an example:

    class MyClass
      def public_method
        puts "This is a public method"
      end

      private

      def private_method
        puts "This is a private method"
      end
    end

    obj = MyClass.new
    obj.public_method   # works, prints the string
    obj.private_method  # fails, no method

    irb(main):014:0> obj = MyClass.new
                  => #<MyClass:0x00000001368fd6d0>
    irb(main):015:0> obj.public_method
                  This is a public method
                  => nil
    irb(main):016:0> obj.private_method
    Traceback (most recent call last):
            4: from /usr/bin/irb:23:in `<main>'
            3: from /usr/bin/irb:23:in `load'
            2: from /Library/Ruby/Gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
            1: from (irb):16
    NoMethodError (private method `private_method' called for #<MyClass:0x00000001368fd6d0>)
        Did you mean?  private_methods
    irb(main):017:0> obj.send(:private_method)
        This is a private method
        => nil
    irb(main):018:0>


To be fair, you can also use reflection in Java to call private methods or access private fields, especially since they removed the security managers. Just because there are ways around it doesn't mean "it does nothing".


> I’m surprised they did that in a minor release and not a major one.

I think allowing it on self. can be argued to be a pure feature -addition- rather than a -change- in which case it being in a minor release is reasonable.

(I'm not sure to what extent I buy that argument, but it at least seems like a reasonable one to make)


> Protected methods can be invoked on self or with an implicit receiver.

No, protected method can be called on self or from other instances of the same class. Typically used for implementing `==` methods.

> Private methods can only be invoked on an implicit receiver.

That used to be true, but as others mentioned is no longer true as of Ruby 2.7.


Ruby's behaviour is weird if you're used to many other OO languages, but it shares that weirdness with other languages influenced by Smalltalk, such as the Magik languages used in the Smallworld GIS.

Both ideas of what private means make sense, but I can certainly understand the confusion they cause if you expect one behaviour and discover your language does something different.


That's why I always approach new languages as a complete beginner. It's tempting to jump into the deep end thinking you know the basics, but in my experience it has a lot of pitfalls.

I like to find a book or "web book" with exercises such as http://JavaScript.info and just follow it. Read it and do all the exercises. It takes some time and some times it feels a bit like the exercises are too easy, but often times I find that they are deceivingly complex precisely because I'm misunderstanding something. It's like a win/win, either I do the exercise in a few minutes because it really was easy, or I learn something.

I enjoy entering zen mode and just banging out a ton of exercises. I see a lot of people consuming video tutorials/courses without exercises and I think they're doing themselves a disservice. They get to chill and just watch a video and feel like they're learning but most of the time they really arent. I see people waste hours and hours watching courses or transcribing code from tutorials and the moment they try to do anything on their own they realize they have absolutely no idea what they're doing. It's just a huge waste of time.

People are different and stuff but I don't really buy the whole "people learn differently" thing. Everyone learns best by doing, and the structured "read about a topic, then test your knowledge by completing tailored exercises for that topic" approach allows you to have a linear and thorough path from zero to fairly advanced. I've tried books without exercises and I hated them.

I'm not opposed to looking up videos on specific topics you struggle with, but I think videos are best used as a supplement rather than a primary form of education.


Some of the reason for Scala having `private[this]` is that it allows an implementation that behaves (and performs) like a Java private field. Scala fields generally aren't raw fields at the JVM level (because of Scala's "uniform access principle" that `def x` should be accessible like `val x`), but private[this] fields allow the compiler to represent them as raw fields since they really can't be accessed elsewhere.

The only other thing I'm aware of being allowed to go in the[] part of a Scala access modifier is a containing package (or object?), which allows you to emulate Java's "default" (AKA "package-private") visibility.


That’s cool! Thanks for sharing, I had never considered that aspect of private[this].

Mentioned in the article, but it also powers covariant classes. In a language like C# only interfaces can have covariant type parameters, because it wouldn’t be sound to store a value in a (C#) private field if it had a covariant type. This influences a lot of the API design in C# where basically every container a covariant/immutable interface alongside the invariant/mutable implementation.

(Java gets around this with use site variance annotations which are more flexible but have their own drawbacks).


Python takes an interesting approach that mimics the behaviour of statically-typed languages without static typing.

If you have a member that starts with `__` then it gets rewritten. For example's sake let's say that `self.__x` in class `Parent` becomes `__Parent_x`. Also note that because the resulting name starts with `__` as well you can't easily just do this mangling yourself as writing `p.__Parent_x` in class `Other` will result in `p.__Other_Parent_x`. (But you can easily access via something like `getattr(p, "__Parent_x")`). This means that `self.__x == that.__x` will desugar to something like `self.__Parent_x == that.__Parent_x` so you will be able to access privates of other members of the same class. It also means that subclasses won't be able to access private members because `self.__x` will become `self.__Child_x`.

IIUC JavaScript has something similar for `this.#x`. Although this is implemented much more robustly and doesn't rely on unique class names and I believe is actually "secure" as in no other class can gain access. But I am not up-to-date on JS reflection abilities. The code desugars to something like `const X_PROP = new Symbol()` created once-per-class and accessible only to the class itself, then the accesses become something like `this[X_PROP]`.


Fascinating, thank you for sharing!

More about this Python feature in the docs:

https://docs.python.org/3/tutorial/classes.html#private-vari...


> IIUC JavaScript has something similar for `this.#x`. Although this is implemented much more robustly and doesn't rely on unique class names and I believe is actually "secure" as in no other class can gain access. But I am not up-to-date on JS reflection abilities. The code desugars to something like `const X_PROP = new Symbol()` created once-per-class and accessible only to the class itself, then the accesses become something like `this[X_PROP]`.

Documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

TL;DR it's a language feature, there's no reflection and it's not a syntax sugar for `new Symbol()`. It's actually implemented as a _syntax_ error to access the private member outside the class, which is made possible by using a sigil `#`


yes this makes sense because ruby is a message passing language. even accessors are messages. only self can receive private messages.

and the sharing a private instance variable between separate instances example is exactly what protected does, which ruby also has.


Exactly. And prior to Ruby 2.7, even calling a private method with an explicit `self` as the receiver was not allowed, only the implicit `self` from "within" the class body ("within" in quotes because, as the blog post points out, Ruby's concept of being inside a class body is unlike many other languages).


I don't think this has to be the case! I think you could build a different language with the same message passing semantics as Ruby, but with the limitations that classes are not allowed to be re-opened and method definitions must appear syntactically at the top-level of a class (and maybe a few other limitations), and then build something more similar to how `private` works in a traditional, statically-typed language.

You definitely don't arrive at a language that has all the same feeling of Ruby, but you could still have the "everything is an object, and everything is message passing" character to the language.


> 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.

Rerouting throgh comments: Event if it's very different, TypeScript's private has quite surprising semantics as well...


TypeScript's `private` keyword is like the Pirate's Code:

> More like _guidelines_, than actual rules


I’ll add to existing comments that it might also be interesting to look through conversations re Smalltalk. Ruby was influenced by Smalltalk, so interesting to note in Smalltalk it is impossible to have private methods.

https://stackoverflow.com/q/7399340

http://computer-programming-forum.com/3-smalltalk/5139b6267b...

…etc


protected[this] was removed in scala 3[0]. And honestly, I can't think of a case where it was really useful.

[0]: https://docs.scala-lang.org/scala3/reference/dropped-feature...


Thank you for sharing!

It looks like the Scala project found allowing protected[this] fields as an escape hatch for variance to be unsound:

https://github.com/scala/bug/issues/7093

Going to have to chew on that and what it means for the same feature in Sorbet…

Thanks again for bringing this up!


> The member x cannot be accessed in subclasses of Parent, like Child

This is a typo and should say "can", right?


Yep, another bad copy paste from the previous section. Thanks! Will fix in a bit.




Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: