I think, even in prototype systems, the class-vs-instance dichotomy still exists at a conceptual level, even if the language/runtime doesn't make it explicit. My "window" prototype might be the same sort of thing as an individual window, but it would be a big mistake to try to actually display the prototype on the screen. Being explicit about the class-vs-instance dichotomy is a kind of strong typing – the language and runtime is aware of the conceptual distinction; whereas prototype-based languages are comparatively weak typing – the conceptual distinction still exists in the mind of the developer, but the language/runtime doesn't know about it. Class-based languages catch the "trying to use the prototype as if it was an actual instance" bug at compile-time, prototype-based languages leave it to blow up in bizarre ways at runtime (accidentally treating the prototype as an instance can lead to modifying the prototype, which can then cause unpredictable consequences for all the other objects having that prototype.)
One big problem with Java, is the lack of meta-classes. Why can't I subclass Class? There are cases where doing so might be a sensible solution, but Java won't let you. In Java, for my instance methods, I can inherit them from a superclass, or have them implement an interface; but, static class methods cannot be inherited, nor can the static methods of a class implement an interface. This forces people into workarounds such as the factory pattern (which many Java applications/libraries massively overuse), when having a static method implement an interface might be a simpler solution – but that would require us to be able to (effectively) subclass Class. It also produces weird things like 'Class<T>' to mean a class extending class (or implementing interface) T, where meta-classes would offer a much more elegant solution. I don't think the real problem with Java is that it is class-based rather than prototype-based, I think the problem is that its class system is half-baked (no metaclasses, no reified generics, etc)
A point this article raises, which is very true – in Smalltalk, reflection is read-write – modifying classes at runtime is as easy as querying them. Java reflection is essentially read-only. It is possible to do read-write reflection in Java, but it involves immense complexity with source code or byte code generation libraries, custom class loaders, etc – to do something which Smalltalk supports trivially. The argument on the Java side, is that read-write makes optimisations (JIT/etc) a lot harder. No doubt true, but there are alternatives which Java has not pursued – for example, support multiple versions of a class, code is JITted to work with the latest version at JIT time, but other versions are detected and cause a fallback to interpreter mode (possibly followed by re-JITting to add support for that other version.)
> My "window" prototype might be the same sort of thing as an individual window, but it would be a big mistake to try to actually display the prototype on the screen.
This is not how Self works. In Self, it is legitimate to take an existing window, clone it, and change properties and methods to taste. The first window serves as prototype for the second, despite also being a regular window. It's also possible to use what I call the Master Mold pattern (after the Sentinel from Marvel Comics whose only purpose is to make more Sentinels). That is, to construct an object that isn't actually used except as a prototype for other objects. In that case, the prototype will indeed function like a class. But this isn't necessary or even encouraged in Self.
> In Self, it is legitimate to take an existing window, clone it, and change properties and methods to taste. The first window serves as prototype for the second, despite also being a regular window.
But doesn't this have the risk, that I change some property on the first window, without realising the second window is using the first as its prototype – and while that property change is beneficial for the first window, it is detrimental to the second?
I suppose the same thing can happen in class-based OO – I might make some change to a superclass which is beneficial for one subclass but breaks another. However, I think the class-vs-instance dichotomy helps here – if I'm trying to evaluate whether a change to the superclass might break a subclass, I can focus on the subclasses, and often I can get away with not studying every piece of code which instantiates or uses the class and its subclasses. I can't use that heuristic in prototype-based languages, since the language syntax and semantics don't distinguish unextended uses from extensions.
> It's also possible to use what I call the Master Mold pattern (after the Sentinel from Marvel Comics whose only purpose is to make more Sentinels). That is, to construct an object that isn't actually used except as a prototype for other objects.
Oh sure, but here we are back to "weak-typing" vs "strong-typing". A class-based language enforces the "Master Mold" pattern, and if you try to violate it, your code won't even compile. A prototype-based language doesn't enforce the pattern, it is just in the programmer's head. Being able to break it in an exceptional case might be helpful; on the other hand, there is the risk the programmer might break it by accident rather than intention, producing bugs which would never happen in a more strongly-typed language.
> But doesn't this have the risk, that I change some property on the first window, without realising the second window is using the first as its prototype – and while that property change is beneficial for the first window, it is detrimental to the second?
As I recall in Self, the two objects are independent of each other aside from the second being derived from the first upon its creation. Meaning changing one does not affect the other.
"But what if you want to change something about both windows, or all windows derived from a prototype?" you might say. "You would have to change them all manually, for every single affected window!" To which I say the Self developers apparently thought the rigidity of class hierarchy and the fragile base class problem were bigger problems than the unergonomic nature of making sweeping changes across an entire set of derived objects.
Class-based object systems present shortcomings because you cannot accurately predict a class hierarchy that will solve your problem especially if it is unknown. Even in a language as flexible as Smalltalk, class-hierarchy decisions made before a problem was fully characterized impeded the programmers' ability to fit a solution to the problem. OO developers have learned to accept these shortcomings, and the extra work it takes to work around them. Self is an experiment in eliminating these problems, and it just presents a different set of tradeoffs.
What was really interesting about Self was its optimizability - they were able to implement a Smalltalk on Self that was much faster than the contemporary Smalltalk systems. The impression I got from poking around at Self was that there was a class/inheritance system informally approximated in the prototypical relations of the objects and traits, but that doesn't mean you can't formally define one.
For anyone curious, I recommend this talk [0] by David Ungar, one of Self's creators, that explains the history, philosophy, successes, and failures of the Self project. It's a shame more didn't get done with it (though V8, arguably the most important language runtime, is very similar). Someone has even been making a Zig version of Self recently [1].
>accidentally treating the prototype as an instance can lead to modifying the prototype, which can then cause unpredictable consequences for all the other objects having that prototype
In my head, that's not how prototypes work -- prototypes are objects that serve as a template for other objects. The behavior of the object is defined by its _traits object_ which is pointed to by a parent slot. Therefore there is no difference between modifying the prototype object and its copies - sending the copy message to them will still yield the same result, and one does not affect the other. The only difference is that the prototype object is in a well-known location for copying convenience. Note that this does change once you modify the traits object, but that's not an operation you do lightly anyway.
> Java reflection is essentially read-only.
I believe part of this comes from the bytecode being read-only, no?
> The argument on the Java side, is that read-write makes optimisations (JIT/etc) a lot harder.
> No doubt true, but there are alternatives which Java has not pursued – for example, support multiple versions of a class, code is JITted to work with the latest version at JIT time, but other versions are detected and cause a fallback to interpreter mode
This is already how it works. You can define any method definition at runtime and the JIT will fallback until it can optmise the new version again. What Java doesn't allow is modifying classes or interfaces because that would compromise the type safety of the language, which AFAIK is not a concern in Smalltalk.
One big problem with Java, is the lack of meta-classes. Why can't I subclass Class? There are cases where doing so might be a sensible solution, but Java won't let you. In Java, for my instance methods, I can inherit them from a superclass, or have them implement an interface; but, static class methods cannot be inherited, nor can the static methods of a class implement an interface. This forces people into workarounds such as the factory pattern (which many Java applications/libraries massively overuse), when having a static method implement an interface might be a simpler solution – but that would require us to be able to (effectively) subclass Class. It also produces weird things like 'Class<T>' to mean a class extending class (or implementing interface) T, where meta-classes would offer a much more elegant solution. I don't think the real problem with Java is that it is class-based rather than prototype-based, I think the problem is that its class system is half-baked (no metaclasses, no reified generics, etc)
A point this article raises, which is very true – in Smalltalk, reflection is read-write – modifying classes at runtime is as easy as querying them. Java reflection is essentially read-only. It is possible to do read-write reflection in Java, but it involves immense complexity with source code or byte code generation libraries, custom class loaders, etc – to do something which Smalltalk supports trivially. The argument on the Java side, is that read-write makes optimisations (JIT/etc) a lot harder. No doubt true, but there are alternatives which Java has not pursued – for example, support multiple versions of a class, code is JITted to work with the latest version at JIT time, but other versions are detected and cause a fallback to interpreter mode (possibly followed by re-JITting to add support for that other version.)