Hacker News new | past | comments | ask | show | jobs | submit login
A late-night rant about OOP and parametric dispatch (avdi.org)
38 points by joeyespo on Apr 2, 2016 | hide | past | web | favorite | 32 comments

I don't really see a pervasive point to this rant. It is true that OOP can be reduced to message passing, and that there's a specific style of OOP a lot of people see as "OOP", but that doesn't mean that this style is "just obfuscated procedural code, or uglified functional code". That OOP can be implemented on top of functional style code, or procedural style code says nothing about it being "true OOP" or not. For example, the great Common Lisp Object System is built with functional primitives. And that makes it more elegant, not less. Building an object system on top of a functional/procedural language is syntactic sugar, and there's nothing inherently bad about it. After all we write code for people to read.

I'd like the code complete approach. A method has 4 channels: api, status, input, output.

Reducing how many channels interact semplify code a lot. Oop went for input, status and statu, output pairs. Functional uses input, output and api, output pairs.

Good programmers understand that and apply the one which is proper to model their problem, to reach performance/memory goals and most importantly to reduce te cognitive load of large codebases.

This seems weird to post here. It's clear he's working out some ideas, but hasn't gotten them clear enough in his head to take out of context and discuss in a place like HN.

It does not seem to have been posted by the author.

Consider this tree:

Let's call this the coordinate-system of your API (I've extended it a little hopefully to make it clearer). It's a small coordinate-system because it doesn't take any strings, but whatever. 'loudly' only makes sense in the context of 'bark', and 'bark' only makes sense on the context of 'dog'. That list of nodes is a coordinate that identifies a unique position in the API. (Note that in this API, actually just "loudly" should be enough to call the right code, but that won't be true in general.)

To be formal you can pick out the code by saying `(do dog bark loudly)`, where `do` is a reduction that concatenates and executes the code at each node. (With a little constraining you could say `(do-special loudly)`)

OOP just calls these API coordinates by special impressive-sounding names. The top level "objects" and the second level "methods" and the third level "method arguments", and imposes all kinds of finicky constraints (some of which are even useful). Languages differ wildly with these constraints: Java doesn't let you have a unique instance method (well, not without either reflection or bytecode manipulation, anyway), but Ruby does (lovingly called "monkey-patching"). And it's quite rigid in it's current form, with a fixed noun/verb/adverb structure.

At the end of the day all that detail obscures the simple truth that your code defines a coordinate system and an invocation is a single coordinate. There's a lot more to be said about this, of course! But I hope it helps/inspires.

According to Alan Kay true OO is message based. First you'd identify what the objects are -- it seems dog and cat. Then you have a choice. Can either send them messages which look like {bark, softly}, {bark, loudly}, {run, fast}, {run, slow}. Similar for cat.

Or can think of dog having an internal state machine and can be in two states -- barking or running. When it is barking he can get two messages {softly} or {loudly}. If it is running, can get fast or slow messages. There is a way to switch from one state to another say getting a {bark} or {run}. Other messages might lead to an error/exception.

This way the first way of doing things is just sending a message which both switches state and regulates intensity of activity.

Heck pretty much a similar example is presented in Learn You Some Erlang For Great Good book in the "Rage Against the Finite State Machine":


(My favorite state machine is a that of a cat though in that text)

Yes indeed. But what I'm saying is that there is actually just one nameless state machine whose coordinate system is organized as a tree who's leaves represent the machine's degrees-of-freedom. It's kind of an odd thing: the leaf both partitions state, and it also co-locates the DoF for changing the state (or at least it's apparent state, if you're immutable).

Neat link, I'll have to check it out. I've not done erlang but I'm quite intrigued by gen_server in particular.

Sorry for talking about coordinate-systems: I can't help it, I have a physics degree.

This is good intuition. I tend to use this kind of idea in code these days. Here's an example:

    ($define! tree ($branch
      (dog ($branch
        (bark ($branch
          (loudly ($lambda () "WOOF!"))
          (softly ($lambda () "woof"))))
        (run ($branch
          (fast ())
          (slow ())))))
      (cat ($branch
        (meow ($lambda () "meow!"))
        (purr ())
        (run ())))))

    ($walk tree ($walk dog ($walk bark (loudly)))
    => "WOOF!"
If one attempted to just say (loudly), an error would be raised because loudly only exists in the context of bark, which only exists in the context of dog, etc. We walk through the tree to find this context, then run the function (loudly).

In the above Kernel code, the context is called an environment, and environments are first class. The evaluator takes an expression o and an environment e as arguments, and it is said that "o is evaluated in e".

The tree is basically described by construcing new environments, where for example, the symbol "dog" is bound to another environment containing the bindings "bark" and "run". I've used the word "$branch" in the example for simplicity, but this term already exists in Kernel under another name.

   ($define! $branch $bindings->environment)
$walk is implemented by combining the current environment with the one specified as its first operand, then the second operand is evaluated in the combined environment.

    ($define! $walk
      ($vau (env expr) dynenv 
        (eval expr (make-environment (eval env dynenv) dynenv))))
Programming this way can be pretty fun, as you're not restricted by some arbitrary "impressive sounding names" for accessing environments in restricted ways. You can do things the way you want, and it's fairly trivial to implement your own object systems.

I've termed it environment-oriented-programming. I basically use it as a means to implement OOP, generics, records, algebraic data types and whatnot. When combined with the use of other Kernel features like encapsulation types, it can be used to implement interesting type systems.

Cool beans, sparkie. I'm working on something involving this shape (which is like a set of related call chains). The ability to call "loudly" by itself is a convenience only, a kind of syntactic sugar. I'm sure you could easily write the function that takes this tree, the string 'loudly' and emits '($walk tree ($walk dog ($walk bark (loudly)))'. :)

There's certainly a few ways you could do that in Kernel, but I think doing this is less interesting than having to explicitly specify the environments to enter to find loudly to begin with. I see other potential problems with just specifying loudly alone which we'd look to avoid up-front, or explicitly handle as part of the tree.

So for example if the cat were to become able to meow loudly, there would no longer be just one path into loudly, but we'd have two. Typically, we'd either make loudly a generic and specify (or infer) the type were referring to, or we'd have a dynamically-dispatched loudly which looks at say, the first argument and decides which one to execute. Both the these are trivial to implement as environments. However, the more interesting gain Kernel gives is that there is no way built into the language to enumerate the bindings in an environment (a deliberate omission).

We can walk through the tree as given in this example (using car and cdr), but if there were some binding "(mouse the-mouse)" added to the tree, where the-mouse is already an environment, and not a list of instructions to build a new environment, then we cannot walk through its sub-tree, which may contain bindings such as "loudly".

We can explicitly call ($binds? the-mouse loudly) to test this, iff "the-mouse" is an environment. This works by basically attempting to retreive the binding loudly and capturing the error thrown if it doesn't exist in the binding. In practice, we'd have a more interesting type for mouse, which contains an environment, but encapsulated (via make-encapsulation-type), with only predefined ways of accessing the bindings of the environment.

With this setup, a binding in an environment behaves much like a capability. By knowing the name of a binding, you have the capability to use it. Proper use of the environment and encapsulation types gives us the flexibility to tightly constrain who can see "loudly".

>However, the more interesting gain Kernel gives is that there is no way built into the language to enumerate the bindings in an environment (a deliberate omission).

Why not throw an error, or perhaps return a curry that takes an integer picking between multiple 'loudly' options? Its true that such 'do-special' would need global read access to the entire program, which is perhaps what you want to avoid?

Global access to the entire problem is definitely what we want to avoid. Encapsulation is a useful tool for maintaning invariants, controlling access to data and so forth. We don't want to give it up, and we may not even be able to give it up to begin with.

One way to consider an environment is that it may be a physical machine with a set of bindings, variables and processes on it, where some of the bindings point to other environments - ie, other machines.

Then if you're a user of the tree, and you want to invoke loudly, there exists at least one other machine, dog, between yourself and bark. You obviously don't know what is on the bark machine because the dog mediates your access to it. You don't really have any access to it other than by sending a message to dog which asks it to send a message to bark.

So the number of "loudly" options in such a network (ie, the internet) is potentially unbounded. The dog machine might know, by virtue of asking the machines it is directly connected to, whether the binding loudly exists, and it can chose to expose either the binding, or another message named loudly which discriminates between multiple loudly options if the other machines connected to it (aka, dynamic dispatch).

In the example above, replace "$branch" with "$spawn" and "$walk" with "$send", and you effectively have the actor model, which is more like the OOP imagined by Kay and others. The fun part here would be to implement $spawn and $send though.

Ah, you inspire the thought that we don't necessarily know what the valid inputs are to a distributed program! But realize that this problem will bite you even higher up the food chain (so to speak) because what if there is more than one dog node? What does your program do in that case?

An environment can only bind a symbol to one expression, so the symbol "dog" in the tree environment is bound to an opaque reference to an environment (or machine), containing the dog's bindings. There can be many of these machines containing dog's bindings, but if you were to bind them all to dog, you would only have access to the most recently bound one, since binding a name in an environment overwrites any existing binding with a new one.

The solution is therefore, that we must either use bindings such as "dog1", "dog2" and whatnot, or we have a binding "dogs" which binds a list of environment objects, which we can index to refer to a specific dog. Actually, "dogs" could itself just be an environment which binds specific dog's names, like spike to their respective environments

  ($define! dogs ($bindings->environment (spike ($spawn ...)) (charlie ($spawn ...))))
This might be less useful than a list though as there's no way to map over the dogs in an environment.

The author has no idea what “parametric polymorphism” means. It's not “dispatching on all the arguments to a function”.

What was so urgent about getting out this half-baked thought out that the dude couldn't have waited to write something coherent?

If all you're doing is calling methods on objects with no supertypes or private members, then yeah, you could accomplish the same thing by convention. But once you start layering on more functionality in your object system, the number of way you code encode this functionality starts to explode. Everybody ends up doing things their own way, and there's no consistency or compatibility. Sound familiar? It should because it's C.

Sincere question for you: Why does it bother you so much that someone wrote something about something on the web (as hinted by your opening sentence)? Why should we care that the thought is half-baked? Does it rob you of precious space on the internets?

Sometimes, we just want to use the medium to share thoughts with people, there's no cause to be annoyed or even condescending like that, it makes for a horrible atmosphere where no one will want to engage in public discourse.

Fair point. Maybe I was too harsh. I do believe that people should be able to work out their ideas publicly. And perhaps it's not the author's fault that it ended up on HN.

But the again, if you visit the blog now, the piece reads:

> Update: this post was the product of an exhausted mind, and as such it misused terms, wasn’t well backed-up, and was generally incoherent. I’m withdrawing it until such time as I can articulate these ideas a little more competently.

I look forward to the outcome of this.

And I agree with that. Avdi can be a great layman-oriented writer, he has a knack for vulgarizing the cryptic, that article was not an example of that. It was rushed, obviously. My point was about something else though, which we seem to agree on.

Thanks for your response.

From Alan Kay's quote:


OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.


What is really described here is a dynamicly typed, actor-based language. I can see a bit of truth in Joe Armstrong's joke that Erlang is the original and true OO language.

In a purely linguistic sense, the phrase "Object Oriented" does have some affinity to single dispatch, because the dispatch is oriented around a single special Object.

"Message Oriented" flows well into multiple dispatch, as the entire message can be taken into account for determining the final method combination.

This of course is related back to the notion of a language focusing on the nouns (Objects), or verbs (Methods, Messages): http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom...

> "Message Oriented" flows well into multiple dispatch, as the entire message can be taken into account for determining the final method combination.

I'm not sure about this. The metaphor is that you send a message to a specific recipient, and the recipient figures out, independently, how to behave in response to that message. So it still seems like single dispatch to me.

In Lisp/CLOS-style multiple dispatch, the method you call is not associated with any particular object. It's simply a dispatcher, and the objects are the parameters/message contents.

So you don't send a message to a recipient object, but dispatch it using a toplevel method name. I know that's a deviation from Kay's descriptions, but it meshes well with the examples of multiple dispatch that I've seen.

Conceptually, "dispatch" can be understood as finding the recipient, not an after-receipt task.

Yes, I know how CLOS works. It's certainly a powerful approach, but it doesn't strike me as terribly... umm... object-oriented.

(0) In CLOS, as you say, methods are requests to the global environment to find the right behavior to perform on a list of arguments. In an object-oriented system, each object has its own behavior, and the rest of the program (that is, the environment) has no business knowing how it internally works.

(1) In CLOS, anyone can tamper with any object's slots, and this isn't even discouraged. In an object-oriented system, the internal representation of an object can't be directly manipulated from the rest of the program. The behavior of an object is defined entirely in terms of how it responds to messages.

That being said, you can do object-oriented programming with CLOS. It's just not the only, or even the main style that it supports.

For more information, see: http://wcook.blogspot.com/2012/07/proposal-for-simplified-mo...

> In an object-oriented system, each object has its own behavior, and the rest of the program (that is, the environment) has no business knowing how it internally works.

I would say that describes Erlang-style message passing, or maybe JavaScript-style prototype OO. But in most "common OO" languages, I would say it's more correct that each class has its own behavior, not object. Of course, the hidden state of an object can parameterize the class's behavior.

But still, there are many behaviors which differ based on the type or identity of their arguments, but aren't well suited to be encapsulated into said objects as they're equal peer parameters to the message, hence CLOS etc.

I find clojure's multimethods (http://clojure.org/reference/multimethods) really awesome in the context of dispatching.

Maybe we need some sort of hn robots.txt file to stop things from getting posted before someone's ready for them to be :)

Or an HTTP code. WIP : 218


The issue to me is one of name spacing. Where is the XML#to_s defined? Certainly not in one very large to_s function that takes in large swaths of various types. Of course it should be namespaced around XML, the only issue is whether it's module based or object based.

I think object based is better. Why? Because I can take the object and introspect the methods. Any number of functions can take in an XML object, but there are very few methods that the XML object needs to have. In my repl if I do:

    my_xml.methods - Object.new.methods
I get a list of what I'm expected to do with this object. If I do something along the lines of:

    functions.collect { |f| f.first_argument_can_be? XML }
Then I'm pulling in not just the normal functions, but also anything that consumes _any_ instance of the XML instance, including domain specific ones.

Now, you could argue that all method-like functions should be namespaced to their own module, but now humans have to be trusted to not define these functions outside of that module and they have to be trusted to not define anything else in the module. For what purpose?

Elixir is a functional language, and has the option of modules and protocols so you can define XML#to_string in two ways.

Either as a protocol so you could do to_string(this_xml)

Or as a function within the module so you could do XML.to_string(this_xml)

If you use the module function defintion you can import it into your scope via import XML

then you can write to_string(this_xml) as if it were a protocol.

It's not a binary choice. You could easily have everything be module namespaced and then then allow module functions to be selectively promoted to method invocation. But really, that's no different from requiring all functions to be methods and then selectively 'demoting' some to be static or class functions instead.

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