The Self Programming Language
selflanguage.org
selflanguage.org
I mean if you have an array of things and you want a new instance of such things, it's too unnatural to take one from the array, clone it and change it. It seems more natural to have something class-like: An "empty" prototype and a constructor function.
I guess what I'm wondering is what idioms and patterns are unique to prototypal-inheritance OOP languages.
AFAIK (though my knowledge is limited) in Self there isn't a language-endorsed prototype like in JavaScript, instead an object is just a collection of slots and some slots can be marked as parent slots meaning that whenever slot lookup is made on an object, that object's parent objects will also be looked up. Prototypes are just a convention.
This implies that there isn't a "this is of class foo" connection (outside of manual conventions) and hence you do not have an "array of things" but an "array of objects" and each one of these objects can be anything - it is up to the rest of the code to ensure (or not) any similarity between the objects stored in the array.
FWIW the Self Handbook does seem to have a section on programming style and generally does seem to write a bit about how you are supposed to work with the system.
That's correct, although incomplete: parents are only used when no "own" slot matches a message. Self will also trigger an error if two parents answer to the same message.
> Prototypes are just a convention.
Not so, however "prototype" has a completely different meaning in Self than it has in Javascript: a Self prototype is a mostly set-up object to copy, there is no semantic relation between a prototype and its "instance" afterwards.
In Self, an object built to be set on a parent slot (and shared between multiple sub-objects) will be called either a trait or a mixin (depending on its parent chains, essentially).
Self parent slots are no more and no less than built-in delegation. They can be used for inheritance (traits), they can be used for sharing implementation (mixins) or they can be used in the general context of composition (Go has echoes of this in struct embedding).
That may only feel more natural to you because you are used to a Platonic/Aristotelian view of the (programming) world (https://en.m.wikipedia.org/wiki/Aristotle#Epistemology), where classes are defined as “that what all instances of it share” as opposed to a Wittgensteinian, who argued that there often isn’t anything that all of the instances of a class share. Games, for example, may or may be played on a board, have turns, and may not even have players (https://en.wikipedia.org/wiki/Conway's_Game_of_Life)
Class-based modeling also breaks down when, for example, modeling mammals that may have two heads, four or five legs, defining ‘object of art’ (should, for example, be flexible enough to include some, but definitely not all urinals), etc.
Also, with unica (happens a lot in UI programming, where most dialogs are unique, as https://cdn.preterhuman.net/texts/computing/apple/newton/Pro... argues) why split them into two, the class and its sole instantiation?
Prototypical inheritance allows you to implement class-like behavior in the way you mention, but also is more flexible. One can implement the equivalent of static methods in the prototype, for example, create “once per class” state in intermediate objects, etc.
It is a really long post and heavily references Godel, Escher, Bach but this section I think explains the difference really well:
Hofstadter offers several supporting examples for this thesis, but I'll paraphrase one of my all-time favorites. It goes more or less as follows.
Imagine you're listening to announcers commenting on an NFL (American football) game. They're talking about a new rookie player that you don't know anything about. At this point, the rookie – let's say his name is L.T. – is just an instance of the class "football player" with no differentiation.
The announcers mention that L.T. is a running back: a bit like Emmitt Smith in that he has great speed and balance, and he's great at finding holes in the defense.
At this point, L.T. is basically an "instance" of (or a clone of) Emmitt Smith: he just inherited all of Emmitt's properties, at least the ones that you're familiar with.
Then the announcers add that L.T. is also great at catching the ball, so he's sometimes used as a wide receiver. Oh, and he wears a visor. And he runs like Walter Payton. And so on.
As the announcers add distinguishing attributes, L.T. the Rookie gradually takes shape as a particular entity that relies less and less on the parent class of "football player". He's become a very, very specific football player.
But here's the rub: even though he's a specific instance, you can now use him as a class! If Joe the Rookie comes along next season, the announcers might say: "Joe's a lot like L.T.", and just like that, Joe has inherited all of L.T.'s properties, each of which can be overridden to turn Joe into his own specific, unique instance of a football player.
This is called prototype-based modeling: Emmitt Smith was a prototype for L.T., and L.T. became a prototype for Joe, who in turn can serve as the prototype for someone else.
How would a type system work in that context? Or is there no such thing?
I think I had static type systems in mind when I asked the question, but that was helpful all the same. It seems in the case of Self it does in fact keep the reference around.
I suppose the whole point of a static type system is moot with a fully prototype-based language. Instead maybe you can enforce compile-time checks (contracts?) on the messages an object is expected to receive.
Ha! I did not notice! XP
> I suppose the whole point of a static type system is moot with a fully prototype-based language. Instead maybe you can enforce compile-time checks (contracts?) on the messages an object is expected to receive.
I think if you can do the latter, you can probably do the former. By that I mean you can probably do neither.
If cloning is something you do in runtime, how can you enforce compile-time checks on the messages they're supposed to receive? Such messages are defined at runtime.
2014: https://news.ycombinator.com/item?id=7047953
2010: https://news.ycombinator.com/item?id=1957511 and https://news.ycombinator.com/item?id=1520246
Surely there have been others?
a recent talk from Curry On 2019 https://www.curry-on.org/2019/sessions/the-making-of-a-secur...
Sadly more as a shortcut to getting an "object system" up and running quickly than as a design principle though, most of the interesting bits of Self were left by the wayside in the process (possibly in part to provide something which feels more like Java).
Not to mention the subtle (and not necessarily sensible) terminological swaps e.g. in Self a prototype is a "blueprint" instance, which is already set up with all relevant traits & mixins, and gets copied and modified to get the proper instance.
Copying and modifying the blueprint always did make way more sense. To this day I still don't think I understand exactly how JavaScript's prototypes work, exactly.
You can partially replicate Self's mechanism by performing a regular shallow-copy (e.g. using `Object.assign(target, source)` then explicitly copying over the prototype (that's fallible as Object.assign only copies the values of enumerable own properties so it's not going to copy the properties themselves).
These points covers most of what a prototype is:
1. A prototype is simply another object (or null).
2. Every object can have one (and only one) prototype.
3. When looking up a property that doesn't exist on the object, the object will delegate to its prototype.
4. When calling a function on an object, the `this` keyword will be bound to that object, even if the function is defined in the object's prototype.
A simple example to explain what I mean:
// Provide a default implementation of a kitten
const kitten = {
name: "unnamed :(",
greet() {
// Note that `this` will be bound to
// the object that invoked the method
console.log(`Meow! I'm ${this.name}.`);
}
}
// Create a named kitten.
// `Object.create` creates a new object and sets
// its prototype.
const grabbity = Object.create(kitten);
grabbity.name = "Grabbity";
// Call `greet` on the kittens.
kitten.greet(); // outputs "Meow! I'm unnamed :(."
grabbity.greet(); // outputs "Meow! I'm Grabbity."
The only other thing to mention is something you can probably infer: Every prototype is an object therefore a prototype can have its own prototype (and so on). In this way prototypes can be chained.A short summary of those interesting bits would be a nice read for us JS devs :)
The last 10 years have seen a large influx of users who didn't ever bother to grok the older feature set of JavaScript, and demanded features they had learned in Java and other languages (i.e. class-based inheritance) and language developers have obliged. The result is a language with both systems, and there's enough inconsistencies between the two that it doesn't make sense to use both in the same codebase. My advice would be to stick with the class-based features that are more common/popular today because your coworkers will be familiar with them: mixing in prototypical and class-based systems will just result in an incoherent system. Learn the Self features because they're interesting, but be very cautious about introducing them into modern JS codebases.
There literally were no users when the object system was created.
> they pollute the cleaner prototypical model
Javascript’s « prototypal » model is not in any way clean, and all ES6 does is add useful and convenient syntactic shortcuts. The changes to the underlying model are improvements (eg official read/write access to the one lonely parent slot, ability to create objects without a parent, …)
True, but it's odd that you say that as if it is disagreement with what you quoted.
> Javascript’s « prototypal » model is not in any way clean,
What's not clean about it?
> and all ES6 does is add useful and convenient syntactic shortcuts.
ES6 added shortcuts that are only useful and convenient if you didn't bother to learn the prototypical inheritance way of doing things, and wanted to do the things in the Java class-based style.
> The changes to the underlying model are improvements (eg official read/write access to the one lonely parent slot, ability to create objects without a parent, …)
Sure, agreed I guess.
Soon thereafter PCs became obsessed with being either Unix or VMS, and over time that created a harsh immune response to the influence of Smalltalk/Self because it's such a different paradigm.
It's inherently more portable and composable.
> waiting for compilation
There are many languages that are interpreted that are mainstream.
> that restarts the whole world
You can program in mainstream languages without restarting the world. See skewer-mode[1] for example, for programming in javascript without restarting/refreshing. You can change top-level definitions and send them to override the previous definitions. You can do this with any language that allows overriding functions at runtime, IOW late-binding languages with an eval function, like python and ruby. This way of programming is probably very non-mainstream, though. The reason why it's not more popular is probably because most of us are working with programs that take little to no time to restart and get to the same state. Restarting ensures that whatever state we see in the program is reachable with the current state of the source code.
> Why state of the art is a REPL where immediate feedback is limited to printed text with all the actual values barely reachable?
Again, because that's inherently more portable and composable. What do you mean they "actual values [are] barely reachable"? You can access anything via any REPL I can think of.
Smalltalk (e.g. VisualWorks) felt much faster even though it was rendering components itself. Probably a mixture of optimization of common cases that was missing in Self and certain advanced compilation/interpretation tricks that were also missing. VisualAge felt slower. If you used VisualAge for Java you also were using the Smalltalk UI. (That is, until Micro Edition). Squeak using Morphic was pretty slow in its original non-jit mode but the Smalltalk-80 UI was fast (and quite different from how you expect things to work.)
https://web.archive.org/web/20190417013701/http://handbook.s...