But, as I see it, records and types define only data. The former as an immutable map with pre-defined keys, the latter as a fixed set of data fields. Thus neither are an object.
Protocols define only function signatures, without data. More like interfaces. Thus it too is not an object.
Why am I being pedantic? Because data and functions are never coupled in this case. But in traditional OOP, an object groups data and methods, and are thus coupled together tightly.
The data and methods are coupled in the record definition. The methods depend on having the records fields in scope.
I'd say traits in Rust, typeclasses in Haskell and Scala imlicit views are the equivalent to Clojure/Script protocols. And similarly, those constructs by themself aren't OOP. I think a simple proof is that if they were, then all OOP languages would have them. But because there are OOP languages (the most popular ones at that) which don't have them, then it follows this construct is not fundamental to OOP.
> But a record defines implementations of the protocol methods.
It's better to think of it as a protocol is extended to a type. It is not the record which defines the implementation. The methods don't belong to the record, they are independently defined, they can come from a dependency, they can even be shared with other types and other protocols. This is contrary to objects which define their own methods.
> The data and methods are coupled in the record definition.
Functions are associated with a protocol and a type independently of the record or protocol definition. This association is many to many. The same function can be associated over many protocols and many types. There is no coupling. You can even dissociate them. To add new associations or dissociate existing ones, you don't need to touch the record definition at all.
> The methods depend on having the records fields in scope.
They don't. Record fields aren't scoped to their associated methods. They are always public. There is no self or this scope. They are just normal functions like any other. You don't even need a protocol. Just write a function which takes a record and does something with it. Now if you want to make this function type polymorphic, you can use a protocol.
I'm not sure I'm explaining myself very well. Think of an object. The object defines some data fields. And it defines some methods together. You can't use those methods with other objects. The ownership is tied to the object that defined them. The only way to share the methods is through inheritance. Similarly, you can only manipulate the data fields of an object through the object, because the object owns the access to them. Only if it chooses to make them public or expose a getter/setter are other objects allowed access. This is OOP. If you model your programs with objects like that, you are doing object oriented programming. Data is encapsulated behind objects, and can only be manipulated through sending a message (a method call) to the object. The object defines the set of possible methods for that data. You can't do that in Clojure/Script, because no object like construct exists that would allow you to do it.
Now in Clojure/Script, you have datatypes. That is, data with an associated type. You can create custom ones, such as records. A record is a set of key/value pairs with an associated type. A Person record defines a type Person, with keys Age and Weight and their values. That's all a record does. It can't restrict access to its data, it can't expose methods, and it can't receive messages (method calls). You can now write functions that take input of this new type. Those functions can be defined anywhere. They have no special access to the data inside Person, there's no `this` or `self`. The functions don't live inside the datatype. Now, if you want a common set of functions to work over many different types, you can specify a protocol. That's all a protocol does. This is just functional programming.
Sorry for the length. The distinctions are subtle, couldn't find a way to explain them with less words.
This seems completely circular to me. Your definition of OOP is really "whatever all OOP languages do"?
> I'm not sure I'm explaining myself very well. Think of an object. The object defines some data fields. And it defines some methods together. You can't use those methods with other objects. The ownership is tied to the object that defined them.
In Clojure you can't use a protocol method on a type that doesn't implement that protocol. I don't see it as being that different from D's Uniform Function Call Syntax (a free function can be called as a method of its first argument and vice versa.)
What is OO to you? Is it Class Oriented Programming? Is it a programming paradigm or is it the list of features and practices common to a family of languages? Almost no common OO languages treat method calls as messages. It's just a function call with a (usually) hidden 'this' parameter. Applying a protocol function to a record that implements it isn't really any different, from a perspective of message passing. A message is passed to the record and based on its pedigree it decides how to respond.
Plenty of OO languages don't have private data and allow direct access to object fields. Arguably, by your definition, it isn't OO at all to even provide for public fields. As long as the methods follow the data around or vice versa, in my view, encapsulation has been achieved. Enforcement is another concern, but it's not Jail Oriented Programming so I don't see that enforced encapsulation is a necessary condition for OO.
A record implementing a protocol can receive method calls. All of its method implementations happen with the records own fields in scope (assuming you define them in the defrecord). It doesn't need to restrict access, if you'd like it to have private data.. respect its privacy and don't look.
As I understand it, the state of the art in Class-Oriented Programming has been to define interfaces for everything, and then define classes that implement those interfaces, and then use the interface to type functions and methods. A Trait in Rust or a Protocol in Clojure just knocks out the middle man, the unnecessary and unwanted class. What's left is still OO, you can get rid of the class and all of it's baggage and still have plenty of OO left.
> A record is a set of key/value pairs with an associated type. A Person record defines a type Person, with keys Age and Weight and their values. That's all a record does. It can't restrict access to its data, it can't expose methods, and it can't receive messages (method calls). You can now write functions that take input of this new type. Those functions can be defined anywhere. They have no special access to the data inside Person, there's no `this` or `self`. The functions don't live inside the datatype. Now, if you want a common set of functions to work over many different types, you can specify a protocol. That's all a protocol does. This is just functional programming.
This, to me, is a description of object-oriented programming without the class ceremony. It's also functional programming. The use of records and protocols is functional programming, but the constructs themselves have OO nature. I don't think functional programming and object oriented programming are in opposition to each other. I think functional programming and mutable state heavy/class-happy programming don't mix but the latter is not my definition of OO.
It seems logical to me that for two things to be grouped together in the same category, some commonality between the things are required. So there has to be something in common with all OOP languages which makes them OOP. Otherwise, it is not possible for them all to be OOP languages.
You're saying a language supports OOP if it has a protocol like construct. Sure, you can decide to define OOP as such. But then languages commonly refered to as supporting OOP, like Java, get excluded. So it seems to me it's a bad way to define OOP if it excludes Java, which is probably the most cited language when talking about OOP.
> What is OO to you?
It's a paradigm. Where your program models the problem using a concept known as an object. Characterised by the grouping of data fields and procedures responsible for accessing and modifying said fields. Objects know about both their fields and their methods. Where other code can only access the object's fields and the object's methods through a reference to the object itself. A pure OO language would have everything modeled this way exclusively. Thus nothing would exist outside an object, and you'd only have objects interacting with other objects. An impure OO language, thus a multi-paradigm language with support for OO, would allow part of your code and data to be modeled this way through objects, and other parts using other paradigms.
Without resorting to interop, Clojure/Script, to my knowledge, does not have an object construct which fits this definition.
> from a perspective of message passing. A message is passed to the record and based on its pedigree it decides how to respond
The call isn't made to the record, but to the protocol function. A function is called, and this function decides what to do based on the type of the first argument.
> Plenty of OO languages don't have private data and allow direct access to object fields. Arguably, by your definition, it isn't OO at all to even provide for public fields
Those are impure OO languages, they are multi-paradigm. If you were to make all your data inside objects public, and never put any methods on the objects. And you would put all your methods outside of objects, or inside objects with no data and have them take objects with data as their first argument, then your program is no longer using the OO paradigm, it is no longer object oriented.
> As long as the methods follow the data around or vice versa, in my view, encapsulation has been achieved
Clojure/Script records don't have their methods following them around. You can define the functions for the protocol in another namespace, and then you have to require them separately. They don't tag along with the record.
> A record implementing a protocol can receive method calls. All of its method implementations happen with the records own fields in scope (assuming you define them in the defrecord)
Again, the call is made to the protocol functions. Simply importing the record does not make these functions exist within your namespace, you need to require them. And these functions are not scoped within the record. They don't see the fields, they need to access them through the record, like any other function would. Nothing is special about these functions. They're just functions that know how to do something to a concrete datatype.
> As I understand it, the state of the art in Class-Oriented Programming has been to define interfaces for everything, and then define classes that implement those interfaces, and then use the interface to type functions and methods. A Trait in Rust or a Protocol in Clojure just knocks out the middle man, the unnecessary and unwanted class. What's left is still OO, you can get rid of the class and all of it's baggage and still have plenty of OO left
No, what you're seeing is the evolution of languages away from OO. It is a direct result of the criticism applied to the OO paradigm, and traditionally OO languages recognising its shortcomings, thus evolving other paradigms or practices which bypass the OO model. All these languages are multi-paradigm in essence now. A trait effectively is an alternate concept from an object. It defines functions interface and implementation separately from the datatype. You can have a language with traits yet without objects, such as Clojure/Script. Such a language is not OOP. You can't take away the concept of objects and still be object oriented. This is just support for type polymorphism, and it's ortgogonal to objects.
I think you might be the one with a circular definition of OO. Because you see things in languages that have support for OOP, does not mean all their features are part of the OOP paradigm.
Take Java, remove its support for interfaces, now ask yourself if it still supports object oriented programming? Yes? Why?
At least, this is my taxonomy. But I feel it is also the most common taxonomy in use. And I also feel it is a more useful taxonomy. Things are more granular, more specific. And within this taxonomy, the criticism against OOP is well founded, and more convincing. And it makes sense then that Rust and Haskell and Clojure/Script do not advertise support for OOP. It makes sense why Java, which lacks Traits, is still OOP, etc.
Just my 2 cents.
The object-like construct in Clojure is an instance of a record or a type that implements a protocol. (A deftyped type can even contain mutable fields!) It doesn't matter where the methods live, they apply only to objects that implement the protocol. That's behavior and data bundled together. As far as I am concerned that is an object. A trait is just a different expression of an object from an instance of a class. The interface being defined separately changes nothing; interfaces/traits/typeclasses offer different advantages than subtype polymorphism and inheritance hierarchies.
Would you say CLOS isn't OO?
A trait isn't an object and neither is a class. An instance of a data type that implements a trait is as much an object as an instance of a class.
Rust, Haskell, and Clojure don't advertise support for OOP because in the common taxonomy an object is defined as "an instance of a class".
> Clojure/Script records don't have their methods following them around. You can define the functions for the protocol in another namespace, and then you have to require them separately. They don't tag along with the record.
> Again, the call is made to the protocol functions. Simply importing the record does not make these functions exist within your namespace, you need to require them. And these functions are not scoped within the record. They don't see the fields, they need to access them through the record, like any other function would. Nothing is special about these functions. They're just functions that know how to do something to a concrete datatype.
1. Nothing is special about methods in "OOP" languages either, aside from the dot operator and implicit this. There are object systems that have neither.
2. You have made a factual error here. An implemention of a protocol on a record or a deftyped type(is there a proper non-generic name for this?) DOES have implicit access to the fields of the record or type.
(defprotocol Container
(inventory [this])
(add [this thing]))
(defrecord Box [contents]
Container
(inventory [this] contents)
(add [this thing]
(update this :contents conj thing)))
boot.user=> (-> (->Box [1 2 3])
(add 4)
(inventory))
;; => [1 2 3 4]
The protocol method clearly has access to the contents field of the record, scoped by the record definition. The 'this' argument is a convenience to allow reconstituting the immutable object for return. You could just as easily have been forced to call the constructor again in order to reconstitute the object. If the record contained an atom it would happily carry around mutable state and there'd be no need to reconstitute anything.Building on my previous example:
(deftype MutableBox [^{:volatile-mutable true} contents]
Container
(inventory [this] contents)
(add [this thing]
(set! contents (conj contents thing))))
boot.user=> (let [b (->MutableBox [1 2 3])]
(add b 4)
(inventory b))
;; => [1 2 3 4]
A type with mutable state, its fields in scope. This isn't even used.You're correct that the call IS made to the protocol method. And yes, not having the protocol in scope prevents from calling the object's methods. One might even say that methods you don't know about and thus can't refer to are encapsulated. It's an extremely flexible object system--one that eschews mutable state and works well with a functional style and prefix syntax--but it's still an object system.
Object isn't a keyword in any of the languages we've mentioned. But we know how to find them and what they are. I don't understand why you can't see this one.
Thank you for pointing this out. I have learned something today. I didn't know that protocols implemented inline within the deftype and defrecord macros were actually given direct access to the fields. And even more surprising to me, that mutable fields on deftype are actually made package restricted. Well then, I have to concede it to you, even within my definition of OOP, deftype is now an equivalent construct to my concept of objects. Those methods are no longer trait like, they're full blown object methods, with special privileges, and tight coupling to the mutable data fields. They can not be defined separately to the type, and a type can not be extended outside its definition to support such methods.
> I am not saying that traits/protocols/interfaces/typeclasses are necessary for OO, I am saying that they are sufficient. > Object isn't a keyword in any of the languages we've mentioned. But we know how to find them and what they are. I don't understand why you can't see this one.
So how would you define OO? I get it your are in the camp that equal polymorphism to OO? And by the way, I do see it. Clojure/Script has object-based constructs. If you think of the more general sense for object, which is identity + attributes, records fit the bill. This is defined here https://en.wikipedia.org/wiki/Object_(computer_science)#Obje.... Now, I also agree with you, it even has a construct that groups data and behavior together in the form of deftype with mutable fields. So at this point, it has support for OOP.
What I'm really arguing is just a taxonomy. I see three distinct characteristics. The grouping of data and behavior together behind an object (OO). The ability to dispatch to different implementations of a procedure based on the datatype (type polymorphism). And the ability to inherit behavior from the behavior of an associated parent (inheritance). And this is my preferred taxonomy.
In that sense, prior to me learning about deftype with mutable fields, I saw Clojure/Script as supporting type polymorphism using either protocols or multi-methods. And inheritance through multi-methods. But I did not see anything to group data and behavior together behind an object. Thus I did not consider it had support for OO.
I know some people say polymorphism alone is the essence of OO. Thus Haskell, Rust, Clojure/Scipt, Go, PureScript, and others are all OO languages. I don't know though, I feel the average dev doesn't think of it that way.
Other people think of OO as the bundling of data and behavior, and that seemed much more reasonable to me. This is how the Gang of Four book defines OO for example. And this is how I like to think of it also.
But, after this conversation, I wonder if I don't prefer the definition from the wiki link I shared. Where OOP is the combination of all three. A single construct which groups data and behavior, and allows polymorphism as well as inheritance. And this is what is known as an object, and all three must be present to be considered OOP. With this definition, I think Clojure/Script wouldn't fit as OOP. Since, and tell me otherwise, deftype doesn't support inheritance. And multi-methods don't group data and behavior.
> I know some people say polymorphism alone is the essence of OO. Thus Haskell, Rust, Clojure/Scipt, Go, PureScript, and others are all OO languages. I don't know though, I feel the average dev doesn't think of it that way.
I don't think that polymorphism alone is sufficient to be considered OO. I think it's important to recognize you can do OOP without much language support, people even do OOP in C. There's probably someone doing OOP in assembly somewhere.
> But, after this conversation, I wonder if I don't prefer the definition from the wiki link I shared. Where OOP is the combination of all three. A single construct which groups data and behavior, and allows polymorphism as well as inheritance. And this is what is known as an object, and all three must be present to be considered OOP. With this definition, I think Clojure/Script wouldn't fit as OOP. Since, and tell me otherwise, deftype doesn't support inheritance. And multi-methods don't group data and behavior.
Lets work with your definition..
You concede that all of those constructs group data and behavior, since they let you define a type's methods in a manner that has access to that type's internal implementation. You concede that they allow polymorphism because you can substitute any type that implements the trait/protocol/interface/typeclass. I don't think subclass inheritance is necessary to OO because it's there as a means of expressing type polymorphism and in the class-bases languages it was for the longest time the only way of expressing type polymorphism. So.. what's the difference between participating in an interface/trait/protocol and inheriting from an abstract class? I don't particularly think there is any (some of these languages even have default implementations, including Haskell), you get subtype polymorphism from either.. so in my view the inheritance requirement is met even without a full-blown class hierarchy or deep prototype chain.
> Those methods are no longer trait like, they're full blown object methods, with special privileges, and tight coupling to the mutable data fields. They can not be defined separately to the type, and a type can not be extended outside its definition to support such methods.
All of the other constructs work the same way, even typeclasses in Haskell. Trait implementations in Rust can have privileged access to private fields. I assume it's the same with Go interfaces. And in Haskell you can't destructure or even construct a type if you don't have access to its constructor--so if you don't export the constructor from your module.. all your fields are private. But you can export a function that constructs the type. You can export the typeclasses. You can export accessors for public fields, and functions to return a modified version of the instance and so forth.
> I know some people say polymorphism alone is the essence of OO. Thus Haskell, Rust, Clojure/Script, Go, PureScript, and others are all OO languages. I don't know though, I feel the average dev doesn't think of it that way.
Well, they're not all OO languages. They all do have language level support for all of the concepts that OOP is made of and you can use them to do Object-Oriented Programming even though some of them are clearly Functional Programming languages. OOP means classes and nothing else in the mind of the average dev. In my opinion it's really just a pattern that you can apply/implement anywhere with more or less support from the language and in sufficiently flexible languages you can even build your own object system and it might even be nice to use. Supporting OOP doesn't make a Functional language not-Functional, or even a multi-paradigm language. No one would call Haskell a multi-paradigm language, yet it has language level support for OOP. Classes and FP don't really mix, but OO in FP works and doesn't have to compromise the FP paradigm. OO isn't something you need to dedicate an entire language to in order to use or benefit from.
For example, is this OO (in pseudo-code):
person = [:person "John" 23];
animal = [:cat "Moonshine" 5];
function talk(object) {
switch(object[0])
case :person
return "I'm " + object[1];
case :cat
return "Miow " + object[1];
}
talk(person);
talk(animal);
In your eyes?So a vtable, a struct/map of functions, a closure around a struct/map of functions, a closure of state, and that sort of thing would be your minimum OO with no real language support.
Can you clarify what counts as "with them"?
For example, does this count #1:
person = [:person "John" 23];
animal = [:cat "Moonshine" 5];
talk-protocol = {:person (object -> "I'm " + object[1];)
:cat (object -> "Miow " + object[1];)
function talk(object) {
call(talk-protocol.get(object[0]));
}
talk(person);
talk(animal);
Or is it more like this #2: person = [:person {:talk (object -> "I'm " + object[2];)}
"John" 23];
animal = [:cat {:talk (object -> "Miow " + object[2];)}
"Moonshine" 5];
call(person[1].get(:talk));
call(animal[1].get(:talk));
Or would you consider both an instance of "bringing their own code with them"?I'm not going to write in that EDN + CoffeeScript pseudocode. I don't really like those kind of types without pattern matching. I do wish JS had keywords though.
Lets use modern JS and pretend that JS objects are simply heterogenous maps of String -> any...
let cat = {
talk: () => "Meow"
};
let person = {
thinkingAbout: "Politics",
talk: (self) => `Have you heard about ${self.thinkingAbout}?`,
thinkAbout: (self, topic) => self.thinkingAbout = topic
};
function invoke(object, method, ...args) {
return object[method](object, ...args);
};
invoke(cat, 'talk');
invoke(person, 'talk');
invoke(person, 'thinkAbout', 'TV');
invoke(person, 'talk');
That'd be one example of a very bootleg object system. If you want examples on how to build an object system so you can do OOP in a language that doesn't intentionally support OO... look into the Common Lisp Object System, or for some object-oriented C code (there's a lot of it out there), or see Chapter 3 of SICP: https://mitpress.mit.edu/sites/default/files/sicp/full-text/...Both of your new examples are more OO than your last one. CLOS is basically something like what we've been doing + way more effort than either of us is going to put into this.
Your switch statement example was such a straw man it's hard to even say what was wrong with it. Polymorphism in OOP replaces switching on type every time you define a method. This isn't a rule, but I think OOP should be on the opposite side of solutions to the expression problem from switch statements and pattern matching.
Again: In the mind of the average dev any construct that doesn't exactly mirror the semantics of classes isn't OOP. There are people who didn't consider JS to be OO because it didn't have classes. (It still doesn't, but it pretends to so they say "now its OO".) I really can't stress this enough. The languages we were talking about before aren't extreme straw man examples of OOP, like these snippets are.
EDIT: I should add with regard to these examples that I don't think it's necessary that data and behavior be grouped in definition as long as they can be treated so at the site of invocation even if the methods are invoked in the same manner as free functions.
> I'm not sure I really understand the point of this exercise but I'll play along
I'm just trying to see what are the concrete characteristics that are relevant to you to classify something as OO. Would be great if you could list them out. But I'm trying to reverse engineer them right now.
With my prior examples, I was demonstrating the difference I see between OO and a functional approach to type polymorphism.
In #1, data and behavior is grouped together inside the object. Thus you access the object to look up methods to call. The object is thus front and center.
In #2, data and behavior are not grouped. The function is front and center. You first call the function, and it looks up an implementation for the type of its argument(s).
To me, this ordering matters. It's the difference between FP and OO in my eyes.
Similarly, I don't consider #2 to have objects. There's just tagged records and functions. While #1 has what I consider objects, a structure capable of grouping data and behavior.
This order is even reflected in the syntax differences between OOP and FP. Where in OOP you use "noun.verb", because the method is on the object, and you go through the object to get to it. While in FP you use "verb nouns", because the function is first class, and it's the one doing the dispatch, the nouns are just dumb data.
If you look at this, it's also obvious why FP languages are more likely to support multiple dispatch. The function can easily choose an implementation based on all its arguments.
> Both of your new examples are more OO than your last one.
What makes it so? It's especially confusing to me because you also said:
> I should add with regard to these examples that I don't think it's necessary that data and behavior be grouped in definition as long as they can be treated so at the site of invocation even if the methods are invoked in the same manner as free functions.
And this is true of my function that has a switch/case in it.
Is OO the act of looking up functions to call in a datastructure based on type?
This is the only definition that seems to not contradict with what I feel you're saying.
OOP isn't specifically attached to tying behaviour and data together. But if you only get to interact with state via message sends, and you try and follow that all the way through so your only non-primitive values are objects (and perhaps even your primitive values are objects too), then at least some of those opaque references are going to be mutable. And then you get state into the mix, and trouble starts brewing.
IMO Rust traits, Go interfaces, Java interfaces are the essence of OO.
Some people say the essence of OOP is polymorphism. Others say it is inheritance. Others say it is the tying together of data and behavior.
I personally find issue with saying OOP is just ADTs + polymorphism. Which is what rust traits, go interfaces and java interfaces are.
Type A
Type B
function F(value) {
switch(typeof value) {
case A: "I am A."
case B: "I am B."
}
}
There, I have achieved object oriented programming. I have values with an identity, A or B, and I have polymorphic functions over them.I don't know, it feels too broad to be useful.
That's why I prefer to say that OOP is more related to the tying together of data and behavior behind an object. This is how the Gang of Four book defines OO.
I find it frames things in a way that is more differentiating.
So now I can have OO, inheritance and polymorphism in any combination I want. This means Clojure/Script can now be defined as supporting inheritance and polymorphism, which it does, yet not supporting OO. And I can have another language supporting OO and polymorphism, but not inheritance, such as Rust. Or without OO and inheritance, but with polymorphism like Go, etc.
That book defines OO from the point of view of Smalltalk and C++.
"In his 1997 talk at OOPSLA, Alan Kay called it "the best book anybody's written in ten years", and contended that it contained "some of the most profound insights, and the most practical insights about OOP", but was dismayed that it was written in a highly Lisp-centric and CLOS-specific fashion, calling it "a hard book for most people to read; if you don't know the Lisp culture, it's very hard to read"."