The Peculiarities of the Anoma Level Object System
The Object system that is in the works for Anoma Level (AL) is a peculiar to say the least let me first outline the features of our object system:
It’s a true object system, meaning that Object Oriented in the William Cook sense 1. I.E. Objects only know themselves and they work only on message sends. However we do go further away from Cook in how methods themselves operate and specialize.
The object system mostly takes a declarative form, meaning that we will have unification of objects (see 2 for a declarative OO system).
Further our object system is mostly a functional one, we require tail calls 3, and mutation is an abstraction ontop of transition between multiple immutable objects (further work is required to see how this works in practice and protocols ontop).
Methods do not live within a single object. Our system is not self/smalltalk like where methods belong with the object and are lookedup directly from a dictionary object message send. It’s more similar to CL’s applicable methods 4. Currently we are not in agreement where the methods live, other than there will be a standard place the system looks up for the default method to apply.
We have multiple inheritance, and likely mixin style (:around, :after, and :before) parameters to methods. We may recontextualize the mixin style around aspect oriented programming advancements.
Objects themselves have a validity constraint that determines if some instantiation of a class is truly a valid member of that class type (I.E. for the Integer Class [classes are meta objects, so the Integer meta object], we may confirm that the slot with data really has an integer and not a string. Creating a string integer would be rejected by the system and considered invalid).
Methods also have validity constraints, this is akin to constraints in the declarative sense, but they can chose what kinds of objects they are applicable on.
Methods can specialize on multiple objects.
Methods defined on the Integer class can be applied to a completely unrelated class such as a String, however it will likely fail. Both actual integers pose constraints and the method imposes constraints, meaning that the method most likely fails to be applicable. However if the data have compatible constraints the method can effectively go off.
Methods can be searched for. For example (+ 2 3) follows the applicable rules 4, however if we call (integer64:+ 2 3), then we are specifically declaring we are using the + defined for the integer64 class, which imposes constraints on the inputs and output sizes, whereas the first would most likely find bignum:+ which has less constraints (given that 2 and 3 are bignums and not integer64s).
The object system will have a proper reflection point, in order to call something like (class-of some-object), reflect must be called (class-of (reflect some-object)) (we can have short hands but we must call reflect before going to the meta level).
Having a reflect system lets us chose compilers in system, as before sending off an object between controllers we can talk about that at the meta level and even select the format (cairo resource, nock resource, risc0 resource. Or if to another local domain that is trusted just a serialization format for the object).
Having reflect let us talk about information flow control in a principled way, as information flow is a meta property of the object.
Overall we have a very weird object system, it’s an odd fusion of CL and Smalltalk style Object Orientation. There is some unique aspects as well such as messages using the object inheritance chain being applicable to objects in a different inheritance chain! However for more specalized interfaces (such as an actor), we can simply add the validity to the object that it must be from the known set defined for it, meaning that this feature can easily be shutdown via the validity of particular objects.
Thanks for the writeup! I found it generally helpful in understanding what you’re going for.
What does this mean precisely? I skimmed LogTalk but I couldn’t find this concept specifically.
If methods are defined separately from objects, how do they relate to the type of the object’s state (which should, I assume, be encapsulated)? Do methods (defined externally to objects) still reference state types explicitly, and they’re only compatible with objects of the explicit state type? Or do they reference some interface which a state type must satisfy (which seems to me like we’re just smuggling in another object in disguise)?
I didn’t follow this bit - can you explicate further and/or give an example?
Meaning we have prolog style unification, namely on objects themselves.
What do you mean by the type of state? There are slots on objects typically yes, however user facing slot accessors are typically derived from more fundamental protocols. I.E. in common lisp methods don’t like on the object, you can access the slots via the normal slot accessors that were mentioned in the defclass (even make this pattern match if you wish) . Thus even methods in CLOS don’t have special access to state. We do this as it makes the RM easier, and this is a valid style of OO (see the school of OO surrounding the MOP).
When one defines a method/message, they may assume certain other methods/messages that are sent to the particular object. Each method/message may lay out it’s own constraint, in which we can use resolution to find properly given the specific type of object. Which is al very natural. Let us imagine this in smalltalk where not all methods live on the object.
Assume we have an Object O, with methods Ms.
Assume we are a consumer of this Object C.
The user defines a new method M’ which takes O and produces some other Object, O’.
Thus C would like to invoke O M'. However the user can not have M' live on the object directly.
Smalltalk lets users do this by having an extension dictionary
In smalltalk objects live in a package (which is an object), and typically the methods live with the object in the package. However users outside can extend the object’s interface, however they do not live in the same package. Instead of being a bad OO system like Java (we see the hell that this leads in rust), the user can instead have in a package register what extension methods exist on the system and define these methods whenever they want, note these methods have access to the state type that you mentioned before (implying the former semantics). But depending on if we go with more CL style then the slots should expose themselves using a more primitive API the user should almost never touch. This is a difference in design, but I know factor has moved to first class slots like CL (smalltalk 80 doesn’t have slots only fields, but pharo added proper slots to the language).
See
2 is likely a bignum, integer64 is likely somewhere else entirely on the inheritance chain
Note for self: if we have free variables with constraints, the runtime will automatically search for ways to fill them.
LogTalk extends this to search for any object which fulfills a protocol. e.g. could be searching for kudos owned by me which can be spent in such a fashion.
Note for self: hierarchies of protocols, which methods depend on other methods.
Outstanding question: upgrade protocols for class changes. Immutable classes, specific upgrade protocol. References: 1, 2, 3.
I believe we can do almost everything on your list using the declarative nature of purely functional objects, as exemplified here. Or at least, it’s a good basis for your ALO system.
Yes, the linked model is functional, using mutation of objects via polymorphic record updates (part of the type system).
Methods are separated from the object’s representation (data structure) via an interface type. Their class is also separated from the object, but this one contains the method implementations.
Here is an issue. The model for the example I shared does not support multiple inheritance. We need something more, in the direction of intersection types. So, this requirement needs to be investigated if you want that. I believe with support for simple subclasses it’s enough, I might not see the whole picture.
Validity constraints can be baked into the objects functions and there is flexibility here in what you can do. In the example for resources, I mentioned, one can put an internal instance variable to track access to the object, so that one can check that kind of thing at the moment of using methods. Or, at the moment of creation, you can check that the parameters given, the components of the object, follow a particular specification. I’d actually say, we simply use refinement types for this purpose.
Methods can specialise on multiple objects via subtyping of interface types.
Numeral 9 might be problematic. Failure semantics need attention, and the linked model does not currently support them.
Numeral 10. We can write a type inference algorithm for this, most likely. Or it is out there already, probably.
Numeral 11. We can extend the model so that another field, hidden in the object representation, captures an optional class object, which can be returned via a method. That needs to be included as part of the interface type when defining the class for those objects.