A late-night rant about OOP and parametric dispatch
devblog.avdi.org
devblog.avdi.org
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.
dog
bark
softly
loudly
run
fast
slow
cat
meow
purr
run
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.
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":
http://learnyousomeerlang.com/finite-state-machines
(My favorite state machine is a that of a cat though in that text)
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.
($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.
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".
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?
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.
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.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.
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.
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.
Thanks for your response.
---
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.
"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...
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.
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.
(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...
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 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?
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.