Hacker News new | ask | show | jobs
by randomdata 2619 days ago
I think the spirit of OO, an object has agency over how the message is interpreted in order for it to be considered a message. If the caller has already determined for the object that it is going to call a method then the object has lost that agency. In a 'true OO' language an object may choose to invoke a method that corresponds to the details within the message, but that is not for the caller to decide.

Consider the following Ruby code:

    class MyClass
      def foo
        'bar'
      end
    end

    class MyClass
      def method_missing(name, *args, &block)
        if name == :foo
          return 'bar'
        end
        super
      end
    end
To the outside observer, the two classes are effectively equivalent. Since, conceptually, a caller only sends a message `foo`, rather than calling a method named `foo`, the two classes are able to make choices about how to handle the message. In the first case that is as simple as invoking the method of the same name, but in the second case it decides to perform a comparison on the message instead. With reception of a message, it is free to make that choice. To the caller, it does not matter.

If the caller dug into the MyClass object, found the `foo` function pointer, and jumped into that function then it would sidestep the message passing step, which is exactly how some languages are implemented. In the spirit of OO, I am not sure we should consider such languages to be message passing, even though they do allow methods to be called.

3 comments

Is it unreasonable to think of the method as a semantic "port" to which messages (arguments) are passed?

And languages that allow programmers to bypass OO with jmp instructions seem multiparadigm rather than not-OO...

> semantic "port"

Not unreasonable at all! In fact the term used in objective C and Ruby is “selector”. Beneath the synchronous veneer anyway.

vtables is an implementation detail. To compile Ruby with vtables, consider this:

    class A
      def foo; end
    end
    
    class B < A
      def foo; end
      def bar; end
    end
Now you make a vtable for class A that looks conceptually something like this:

    slot for foo = address_of(A#foo)
    slot for bar = method_missing_thunk(:bar)
And a vtable for class B that looks like this:

    slot for foo = address_of(B#foo)
    slot for bar = address_of(B#bar)
The point being that you can see every name used in a method call statically during parsing, and can add entries like `method_missing_thunk(:bar)` to the vtable, that just pushes the corresponding symbol onto the stack and calls a method_missing handler that tries to send method_missing to the objects.

You still need to handle #send, but you can do that by keeping a mapping of symbols => vtable offset. Any symbol that is not found should trigger method_missing; that handles any dynamically constructed names, and also allows for dynamically constructed methods with names that have not been seen as normal method calls.

When I started experimenting with my Ruby compiler, I worried that this would waste too much space, since Ruby's class hierarchy is globally rooted and so without complicated extra analysis to chop it apart every vtable ends up containing slots for every method name seen in the entire program, but in practice it seems like you need to get to systems with really huge amounts of classes before it becomes a real problem, as so many method names gets reused. Even then you can just cap the number of names you put in the vtables, and fall back to the more expensive dispatch mechanism for methods you think will be called less frequently.

(redefining methods works by propagating the new pointer downwards until you find one that is overridden - you can tell it's overridden because it's different than the pointer at the site where you started propagating the redefined method downwards; so this trades off cost of method calls with potentially more expensive method re-definition)

What is the advantage of doing that instead of using an IObservable that can filter on the event name in C# or, even better in F#, having an exhaustive pattern match that automatically casts the argument to the expected type and notifies you at compile time if you forgot to handle some cases?