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