Classes are closures just as much as closures are classes (as Java showed).
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.
> 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.
Closures are the superior mechanism though, you can implement classes plus all the crazy decorators / metaclasses / mixins / etc. The reverse is true of course but in the dumbest way possible.
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.
> 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.
Using C, which doesn't even have closures, as an example of why this doesn't work is rather silly. How can you have closure-based classes without closures? The ergonomics are beyond horrific in this case but I don't see how that's relevant.
> 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.
> I'm not arguing ergonomics, just implementation details, you can add all the syntax sugar you want :)
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?
- 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.
Yeah your first approach to runtime dispatch is just dumb (don't listen to those silly people), you want the second in all cases, even with a closure implementation of classes. Objects should be implemented as in the second case, or more concretely, as:
struct MyObj {
vtable: MyInterface*
data: void*
}
Which is how dynamic traits are implemented in Rust. The C++ approach of a pointer to:
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.
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.