Classes are not a design inevitability, but just one way of managing state. Languages that mostly avoid mutable state don't tend to have object systems, for instance.
Classes are not a design inevitability, but just one way of managing state. Languages that mostly avoid mutable state don't tend to have object systems, for instance.
What "Classes" are is a tragedy of things.
Classes are closures, with a syntactic form that allows for nested composition. This is a particularly brittle form and these qualities are found in all languages with Classes. Language maintainers, as they currently exist, have failed to concede that this is a bad idea, because "it works good enough for lots of cases". This has hurt the industry by creating a culture that can only support the view that "all code is ugly". Given the choices, the variability in how code evolves randomly to deal with choices made under these conditions, is unsurprising.
Why? Because Classes are a good feature that has been misrepresented as a compositional tool instead of a syntactical shortcut and nobody is dealing with the gravity of that...given the amount of history, industry, effort, et al based around supporting their faux necessity, who could be blamed for not raising a stink? And why is there a "new" keyword in 2022? Python gets this right.
Almost every language ends up heaping additional syntax for behavior that is either compiler added (eg php/rust traits, ruby aspects) a meta-language (eg Java aspects, php attributes, various C++ impls, ES6 javascript classes), or a straight mixin syntax (Python decorator). This alone, is obvious that the support for composition is a major problem that has both been neglected, then mishandled by heaping on the same mistakes. Interfaces, were a particularly laughable solution, which has made the problem worse by tying that limited form of composition with reflection.
> Languages that mostly avoid mutable state don't tend to have object systems, for instance.
They do, in the form of closures. How infrequently composition-by-nesting is used in functional languages is telling (basically the inverse of nonFP languages).
What classes are at their most core is bundles of behavior, possibly with encapsulated state, and usually some way to decide at runtime which of a family of classes' behavior to use. Closures are naturally tied to 1 single behavior (though of course you can invent ad-hoc methods of extending that, such as a "methodName" parameter, but no one ever does this in practice).
This is the reason why classes and interfaces actually exist in some form or another in all useful languages today. For example, Go has structs with methods + interfaces, Rust has structs + traits, OCaml has modules + module types (well, it also has classes, but I understand those are less popular), Haskell has data types and type classes, and on and on, JavaScript has objects + dynamic name resolution.
The one idea from classic OOP that has turned out to mostly be a dead-end is implementation inheritance + subtyping. This is basically the only class feature you'll only find in specific OO concepts, and basically all modern guides tell you to avoid it.
If only... There's still a thriving OO-heavy mindset in much of the Java ecosystem, but less evangelical that it used to be.
In other languages... Inheritance tends to be de-emphasised compared to a decade ago, thankfully. I rarely see advice to avoid it - instead moderation is advised.
I think the main problem is that it is hard to explain to a learner what the pitfalls are with inheritance-heavy OO design except in the most general terms. It's something that can be quite useful when used judiciously, in the right context, but knowing when this is requires experience.
A closure-based implementation of a class is just a function that returns a structure full of closures (an interface if you will) all with the same shared state.
But now because a class is just a function, you can do all kinds of crazy things with it, the "constructor" can even pick and choose which functions to use to implement the interface at runtime, while retaining full typesafety.
Classes are not just unnecessary, they're a bad abstraction.
And yet, there are approximately 0 examples of the "closure-backed classes" example you give, while every vaguely popular language except C has some variant of polymorphic classes, from StandardML to Lisp.
Your closure example is not used in any langauge because it's extremely un-ergonomic. If the langauge doesn't have some kind of concept of "interface" (abstract class, type class, trait, structural typing etc), then you'll either miss polymorphism (return a single specific type), or be too dynamic (return an object that could do anything). It also risks being inefficient if you try to naively add function pointer members to each object to support runtime dispatch.
The closure-based alternative also obscures the relationship between product data types (structs/record types) and classes (structs with methods).
Closure-backed classes are a neat experiment, but just like manual virtual table management in C, they are not a realistic solution to the problems that classes address, if you can possibly avoid them.
> And yet, there are approximately 0 examples of the "closure-backed classes" example you give, while every vaguely popular language except C has some variant of polymorphic classes, from StandardML to Lisp.
SML is actually one of the best languages to implement this approach! And as far as I know this is even the recommended way to do it (see the MLton docs). No idea what polymorphic classes you're thinking of when it comes to SML. Nobody uses objects in OCaml either.
> Your closure example is not used in any langauge because it's extremely un-ergonomic.
I'm not arguing ergonomics, just implementation details, you can add all the syntax sugar you want :) Also Erlang Actors are functions with an explicit message loop. So that's even lower level than this.
> If the langauge doesn't have some kind of concept of "interface" (abstract class, type class, trait, structural typing etc), then you'll either miss polymorphism (return a single specific type), or be too dynamic (return an object that could do anything).
I don't follow, neither of those are true. Yes you return a single type, that single type is the interface type (which can have infinite implementations, just like in any language where you return an interface). But you don't need a specific concept of an interface, it's just a record of closures.
Also every language should have a way to say "this struct is the union of these structs", which gives you the subtyping you're typically missing with this approach (i.e. an object that implements N "interfaces" can be cast to all of them).
Without that you need to add an extra layer of indirection with explicit casting functions. Not ergonomic as you say.
> It also risks being inefficient if you try to naively add function pointer members to each object to support runtime dispatch.
If you're not using function pointers then you don't have runtime polymorphism, what do you think a vtable is? It's a struct of function pointers. An object is a pair (voidptr, vtableptr), where the vtable can be created at compile time or at program start. Of course that's not very ergonomic, but I'm not arguing ergonomics.
> The closure-based alternative also obscures the relationship between product data types (structs/record types) and classes (structs with methods).
Classes obscure the relationship between subtyping, interfaces, inheritance, abstract datatypes and modules. A class is much more than a struct with methods. A struct with methods is vtable.
> Closure-backed classes are a neat experiment, but just like manual virtual table management in C, they are not a realistic solution to the problems that classes address, if you can possibly avoid them.
What problems do classes address? Nobody seems to have an answer to this, which is why modern languages like Rust and Go don't have them. You can argue that traits and interfaces are are "OO", but they're not classes. Rust doesn't let me do Iterable.new{...} at runtime, exposing it's nature as a struct of functions, but nothing says it couldn't.
- expose multiple function (implementations) as a single unit
- be chosen at runtime as a representation of a contract (polymorphism)
- (optionally) encapsulate some state accessible only to the functions of the class
This construct comes up naturally in a wide variety of programming tasks, mainly related to the large-scale organization of code.
Here are some examples of language constructs that are classes (perhaps by other names):
- `class` in C++, Java, C#, OCaml etc.; polymorphism achieved through interfaces/pure-virtual classes
- `class` in Python, Ruby; polymorphism achieved through dynamic dispatch
- `defmethod`+`defclass` in Common Lisp CLOS; polymorphism achieved through `defgeneric` (a rare example where polymorphism is at the single function level, instead of a bundle of functions)
- `structure`&functors in SML; polymorphism is achieved through `signature`
- `module` in OCaml; polymorphism achieved through the use of `module types`
- `struct` in Go; polymorphism achieved through use of `interface`
- `struct` in Rust; polymorphism achieved through `trait`
- `data` in Haskell; polymorhpism achieved through `class` and `instance`
- `typename` in the C++ template meta-language; polymorphism achieved through dynamic typing (SFINAE) or `concepts`
Now, some of these languages/constructs may have additional properties. Some support subtyping, some support multiple fields etc. Some explicitly implement interfaces, some implement them implicitly. But they all have the initial three properties in common, and recognizably solving the exact same problem in slightly different ways.
The implementation varies quite significantly, but I would be surprised if even one of these languages actually implements this construct using closures.
Now, to address some of the other points:
> If you're not using function pointers then you don't have runtime polymorphism, what do you think a vtable is?
I have often seen proposals to implement runtime dispatch via something like:
struct Obj {
foo: fn () -> int
bar: fn int -> int
field1: int
}
This is wildly inefficient compared to the vtable approach, which is closer to struct ClassA { //vtable
foo: fn () -> int
bar: fn int -> int
}
struct Obj {
vtable: &ClassA
field1: int
}
> But you don't need a specific concept of an interface, it's just a record of closures.If your language doesn't have a concept like Interface to begin with, you can't implement it out of thin air. There are few languages that don't have this. C is perhaps the only mainstream example, and indeed in C (even if you had closures) you couldn't return an object from a closure that is not a specific concrete type; or void* and expect people to cast to something else.
If your language does have interfaces, it will also have a better way to define classes corresponding to those interfaces than random closures returning interface types.
> I'm not arguing ergonomics, just implementation details, you can add all the syntax sugar you want
I've mentioned this before, but I don't know of a single language where the class mechanism is actually implemented in terms of closures. Typically there are other specialized constructs at the compiler level used specifically for this.
struct MyObj {
vtable: MyInterface*
data: void*
}
Which is how dynamic traits are implemented in Rust. The C++ approach of a pointer to: struct Obj {
vtable: ...
field1: T1
field2: T2
field3: T3
}
Pretty much means you need to define all the interfaces you implemente on class declaration, which is not nearly as nice as doing it independently (as with traits, protocols, typeclasses, go interfaces, etc.)My point is that MyInterface is just a vtable, a class is a function that fills in that vtable, an object is a pair of a filled in vtable and data. These mechanism are restricted to the compiler for no good reason, which is why you end up with "metaclasses" and "mixins" and all that other nonsense. Just expose the mechanism itself and give it some nice syntax.
Classes mix too much stuff together: inheritance, subtyping, access control, data specifications, interface specifications, etc. We've figured out that's not really a good thing which is why Go and Rust rightfully split them up and use separate mechanisms.
I'm saying go even further and don't hide the runtime representation, let me create these things at runtime if I so wish. That gives metaclasses and mixins and what not for free (with a constexpr-like mechanism they can even be done at compile time).
You can still keep traits as the way of defining bundles of methods, they're also useful as generic concepts, that goes into the ergonomics of it.
This is a similar discussion to Zig generics. Zig has no idea what generics are, a "generic" is just a function that given a type produces another type. Works pretty well.
> A class is any construct that can:
That's just your definition. Your points describe an abstract datatype, a concept that predates classes. We could call that a class these days, but that's not reflective of the programming language construct called "class" as used in language like C++, Java, C# and friends.
Oh, good. That sugar actually already exists in most OO languages, and usually it's called...
class
> What problems do classes address?The problem of getting sweet ergonomic syntax for the concepts of encapsulation, polymorphism, possibly inheritance (with additional sub-divisions mainly on whether multiple- or single-ancestor), interface, abstract class, type class, trait, structural typing, etc, etc. Currently implemented in varying combinations and to varying degrees in different languages.
So make up your mind: Do you have something against syntactical sugar for all those, or don't you?
> They do, in the form of closures.
You can build an object system from closures, certainly, but I wouldn't say closures are objects by themselves. An closure lacks inbuilt messaging, which is usually considered a requirement for an object.
This raises 2 issues.
1. Is a closure an object? An "object" is conceptual, not based on a specific algorithm.
Then you mention a capability of messaging being a requirement for the Object concept.
This is where arguments start going off the rails in casual language discussion because people combine the idea of what a language can do (capability), what a language was designed to do (designed-for) with the supporting ecosystem, and what's possible in a turing machine by virtue of what other languages provide (in capability/designed-for).
You can message to other closures via methods, just like the vast majority of languages currently support. This kind of messaging is not async, which seems like a stupid cheat on the idea of a message, but it makes sense once you start running lots of synchronous objects with independent lifecycles, locations, and identity characteristics...then you realize the quick cheat costs you in large distributed systems while being very efficient in smaller systems, imo.
A closure lacks messaging by default, so it's not an object. You can certainly make an object from a closure, and in an object orientated language a closure may be itself represented by an object; but responding to messages is not an inherent property of a closure.
It's not useful to say that all languages have all features; the line has to be drawn somewhere. Being able to theoretically write an object system for a language doesn't make the language object orientated.
But yes, the entire OOP-classes with namspaced methods and "self", that's not inevitable
That has always been in trend in most languages, first one gets modularity, then eventually they outgrown now being able to have generic modules, and eventually they land into something that is for all pratical purposes OOP (there are many ways to skin a cat).
So which languages are left without object systems?
The difference between them are important to some people. And that's why they will argue for that.
So they argue with knowledge based on Internet facts instead of actual CS literature.
Classes binds data and functions together, and object systems supports inheritance. These are some of the things that some people don't really like, but you may not care. People sometimes care about syntax as well, writing "Class" is also part of the experience of using the language.
We have different words in the language for a purpose, that's my opinion.
Not all object systems use inheritance, and even those that do, we can speak about delegation, interface/trait/protocols/type classes inheritance, regular classes inheritance, or some weird stuff like object patterns from BETA.
That is why we have CS literature, to put the right words in place.
As you said, there are a lot of object systems that are different, and the difference matters. Having inheritance or not matters. I was just making examples to show that things are different.
We all know that C++ concepts != go/typescript interfaces != Java/c# interfaces != Haskell type classes (c# concepts) != rust/scala traits.
Try to explain that using "CS literature", maybe a lot of language designs don't have a word yet made for that. Yep there are Subtyping, Bounded quantification, Ad hoc polymorphism, Row polymorphism, but still not enough to tell all of them from each other.
But to anyone in the community, people all know that they are all not Classes and what make them special is important. It is quite clear, but why don't you get that?
The concepts CS literature are quite trivial and people can spend a few weekends to learn, or maybe another few for type rule syntax or a few others for basic category theory to understand them in a more intuitive way. But the real problem of making a good language for some applications is still there and much more complex and difficult. And maybe current literature are not enough for that.
The elites will never dictate the words, the people will pick up whatever they want to express their knowledge anyway, as we know of what happened in history.
As for ML functors: they're neither polymorphism, nor dynamic dispatch. First: functions for different types can't share names (unless we're talking about shadowing of names with lexical scope), the names are either qualified with the name of the struct they come from or they're not qualified at all. In all cases there is exactly one implementation for every name. And again: no dynamic dispatch. MLton even defunctorizes all code at compile-time. The only polymorphism in SML is the one that's used in some built-ins (like +), but it's not available to users of the language. SML has something called "polymorphic functions", but it has nothing to do with writing multiple implementations of one function under one name and letting the compiler select which one is called; which is what is usually meant by "polymorphism".
If I have time later I might post an example, apparently we are only right with code.
At minimum, I'd say an object system needs some form of messaging and state hiding. That is, an object holds hidden state, and responds to external messages.
I advise some reading "The Art of the Metaobject Protocol", or similar literature.
I use Clojure a fair bit, and that has a miniKanren implementation in the form of core.logic. Would I say that makes Clojure a relational programming language? No, I don't think so, because it's not part of the core language.
Similarly, there exists an optional static typing library for Clojure called core.typed - does that make Clojure a statically typed language? Again, I don't believe so.
You can build a library to implement any feature, particularly in Lisps or other languages with support for AST transformations. Those features may even be counter to the core design of the language - you could write a library in Haskell to support dynamically typed, imperative programming, for example.
In some cases, the only way the features of some UTM A's architecture will be obtained through UTM B, is if UTM B is used to create a simulation of UTM A, and so then that provides a way to execute the UTM A instructions themselves. In that situation, UTM B itself has not acquired the features of UTM A.
A UTM can take an instructional input which turns it into a different machine, which can then process tapes designed for a different TM mechanism.
A fixed programming language (and its standard library) is a UTM. If there are regular expressions in the language syntax, or the library, it's a feature of the UTM.
Everything else is instructions: additional libraries, macros, whatever.
We can extend languages by pretending that the additional code we have written (libraries, syntax rewriting) comprise a new, extended language. So that's a new UTM.
There are limits in what syntactic rewriting can provide in a seamless, efficient way. Some features are simply not expressible in the target language of the rewriting, other than by using the syntax to create an inefficient, interpreted language. The use of the features it provides is walled off within the embedded syntax. (Not to deny that this is nevertheless valid and practically useful.)
The UTM theory tells us that we can always adapt a UTM to accept the tapes intended for any TM mechanism. However, it doesn't tell us how costly that is. For some TM mechanisms, the approach may be to translate a given tape up-front into the UTM's own instructions and then run it directly. For some TM mechanisms, the approach may be to simulate the mechanism: the UTM's instructions execute the simulator, which executes the tape. The UTM theory mostly talks about simulation.
What we know is given a Turing Complete language A (i.e. a UTM) we can always write an interpreter for language B. That's the baseline. We then have a new UTM equivalent to B, consisting of language A, plus the interpreter. That setup then accepts programs of language B, and their inputs.
By "the library" do you mean the core library of the language?
If a language didn't have regular expressions as part of its core library, does that mean regular expressions would not be a feature of that language?
> Some features are simply not expressible in the target language of the rewriting, other than by using the syntax to create an inefficient, interpreted language.
Why inefficient? Why interpreted? What if the syntax was efficiently compiled?