The Peculiarities of the Anoma Level Object System

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:

  1. 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.
  2. The object system mostly takes a declarative form, meaning that we will have unification of objects (see 2 for a declarative OO system).
  3. 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).
  4. 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.
  5. 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.
  6. 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).
  7. 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.
  8. Methods can specialize on multiple objects.
  9. 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.
  10. 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).
  11. 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).
  12. 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).
  13. 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.

Appendex

(1) https://pomf2.lain.la/f/3xpqmzlu.pdf
(2) https://logtalk.org/
(3) Why Object-Oriented Languages Need Tail Calls (eighty-twenty news)
(4) 28.1.7.1. Determining the Effective Method

6 Likes

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?

1 Like

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

For example

Here I’ve extended the default string methods with my Mahjong methods. These are not apart of Ms, however I as `C can invoke them as follows:

'123p456s777z' asMJHand

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

1 Like

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.

I don’t believe your model shows this in the way I mean it. Rather what you show is you have a set of methods together which work on some state (why?). The issue is that the “state” of the object hardly matters. There is a state, but you typically don’t access it directly when you have slots. I.E. you send a message to access one’s slots to do operations on in a CL style system (in smalltalk you can access them, but this is morally a message send [pharo extends fields to slots, so I believe this happens under the hood in reality]).

Let us imagine this scenario.

;; Now let us add some generic points

(defmethod add-points (point abstract-point)
  (+ (x point) (y point)))

(defclass abstract-point ())

;; define a class with slots x and y, let's say we don't have any accessor methods yet
(defclass point (abstract-point) x y)

;; make methods to access 
(defmethod x (point point) (access :x point))
(defmethod y (point point) (access :y point))

;; Now let us imagine a different class
(defclass polar (abstract-point) ...)

(defmethod x (polar polar) ...)
(defmethod y (polar polar) ...)

In this scenario add-points will work with either point or polar not because the inner state M is the same, but rather because the message sends these two things accept is the same. In fact add-points is not defined in the set of methods, and can even be added to the method state at a later point. Even how we access the state of the point is via a message send, so likewise x is a stand alone method. Since add-points is defined on abstract-point and in my system could be not in the inheritance hiearchy as well, this should work for either one even if it’s not in the set of valid methods. In your model we can model inheritance

I believe I can make multiple inheritance work, as your model doesn’t even support single inheritance since you aren’t really working with objects, it can be done by smashing together the X and M of your model in interesting ways.

The disconnect

What you posted works well for a formalism of the system. However, I think in an actual implementation this approach will fall short and fail to give you things you want. For example the interface is never declared up front in the examples I give, I defined new methods outside of the scope of a singular compile pass, so it would not be accumulated into a singular M X type. Further the X type does not really exist and is malleable, I.E. we have messages the meta object accepts that can change the type X itself and even update instances to transition over things, further one never works on the full state, rather messages are sent to access particular slots, meaning that even state access is just a message send. There are other fun bits as well, such as in a well designed meta object system, how the type X is saved is determined by meta properties of the object, so X is backed by default in memory but may be backed by a database or sparsely depending on the meta object, in this particular view of X existing without the object you have this scenario where we can not do this kind of modeling.

I believe trying to approach objects in this way is a great way to formalize a subset of the model, however for actual implementation and the various practical engineering properties we would want this model falls short and fails to capture the essence of a sophisticated OO system.

An Unknown semantics on my end

Something I don’t have a good grasp on is if we use a method String >> ++ rather than Integer >> ++, how does the inner message sends work? Will they use the String message sends or the integer message sends when applied to an integer?

@ray do you have a good idea on this point?

  1. It’s a true object system, meaning that Object Oriented in the William Cook sense 1.

I think it’s important to highlight this point and the paper (https://pomf2.lain.la/f/3xpqmzlu.pdf), because the terminological distinction Cook makes there is not mainstream, at least not in the way he puts it. The object system Jonathan proposed, and what the first GOOSE prototype will implement, leans more toward what Cook describes as ADTs, or ADT-object hybrid, at least in terms of presentation. We should perhaps be talking about “Cook objects” to avoid confusion – the distinction with ADT-style object-hybrid is not so commonly made. I understand the ultimate goal of the general Anoma Level Object System is to favour “Cook-style” over “ADT-style”.

However, as Cook himself notes in Section 4 of the paper, this shouldn’t make much fundamental difference for the simple things we’ll be modelling initially (the kinds of apps we want to write with the first GOOSE prototype).

Well yeah if we are trying to compile things to resources, the particulars of ADT vs Object distinction does not matter. This thread is about the longer term vision for what the object system will be and how the final AL object system works. What @jonathan worked on is more formal model of it, which is fine for modeling an object system formally but less useful for an actual implementation. What should always be asked of a formal model is what is it trying to achieve if it’s not fully accurate to the base model. From my understanding the prospects of GOOSE is not to design the object system laid out here, but rather write some of the protocols/mechanics on certain aspects of the object system.

I.E. how do we compile objects to resources/transactions, this is not coupled with the object system, but rather may elucidate certain techniques and features we’d want there, but is a standalone research part that can be taken. Similarly with the History + ID work, it’s not really coupled to any of the properties I’ve mentioned but rather deal with a particular part of the system that we need research to even evaluate how we’d do it.

1 Like

Yes, this seems to be a good summary.

I was saying that Jonathan’s approach doesn’t seem to completely fit what Cook describes as purely object-oriented, because in Jonathan’s model you have concrete state representations abstracted with existential types – something Cook explicitly avoids. So the objects look very much like ADTs / ADT-object hybrid - ultimately you have existentially quantified type of state, internal state representation and some associated methods. Cook models things with recursive types instead.

1 Like

Pierce and Turner have the following summary:

As far as I understood @jonathan was using the approach on the lowest line.

1 Like

The codename GOOSE expands to good object oriented system experience and for fun, there, we could throw in an extra ‘o’ like good old object oriented system experience. The MVP is thus indeed “anything” that qualifies as an object oriented system to be able to write applications. As discussed, this will involve a prototypical syntax, but the core is a compilation to resources such that in particular object state is persisted as non-ephemeral resources on a controller. There is also a rough correspondence between method invocations and their transaction encoding—a term to be defined still rigorously.

This is false. If M₂M₁ (sub-record of methods) you have interface inheritance. “Subclassing” = supply a subtype X₂ <: X₁ and a method table M₂ X₂ whose domain is a superset of M₁. I wrote about this explicitly. What you lack is a convenient surface syntax for incremental extension, not the capability itself, just take that in mind, and ultimately, that’s not relevant from the formal point of view.

Yes! Cook avoids it, but that doesn’t rule out the model. As far as I remember from TAPL, Cooks’ object variants are equivalent to Pierce’s objects. Then, yes, as @Lukasz stated it, say the model I shared sits in Cook’s “ADT–object hybrid” quadrant. And @graphomath is right here as well.

Also

  • We are on the same page, Jeremy, in this regard, and you may not see it. The state is never exposed directly! That’s the whole point of having the existential type.

  • The object-methods cannot be added after object creation. But I guess this okay. The method table lives inside the existential. To support run-time extension you would need either row polymorphism as I mentioned (typescript or ocaml have that I think) or do something inelegant like store the method table in a mutable global registry.

  • Reflection is possible via Pierce self field. See the example with resource objects.

Now, here’s an idea I just came up with. Feel free to ignore it.

What if.. we preserve the existential following the same motivations as for purely functional objects but threat the methods dictionary as another objects and we include info about the object being created with an id for reflection, while mantaing the global registry I mentioned.

Object := \exists X .
          { state   : X
          , primary : DispatchTable 
          , metaId  : ObjectId   
          }

I still need to learn how to make things “useful” in your terms. Still learning.

I understand the state isn’t exposed outside of the system, it’s more of the state is exposed to the methods. This is what I take contention with, this is true for a language like Java where you access the fields directly, however I believe I don’t think this is how languages with reified fields (AKA slots) are defined. Rather than accessing state directly they too go through messages.

This design is much better as it allows slots to be generic, and gives you what you want as well, remember how for engines you defined out immutable state vs mutable state, all of this is uneeded if you have slots as they can dictate if they are read to or if one can write to them.

Thus even to methods, state is not exposed, but rather a set of messages the object must also accept.

Here is some literature on the subject:

This link I found when looking up how they made slots for pharo

This is generic CL documentation covering slots, make sure to check out the later sections as well

However if your attempt is to just to do a formal model when trying to express some properties of a system, then none of this is needed, it’s all gory details of an actual implementation. Objects aren’t implemented in terms of ADTs in reality as ADTs themselves aren’t primitive, and there are much simpler ways to model objects in memory (the self paper gives a fast way to do this that was used to implement JS).

Well it depends on what your goals are here. If your goal is a useful implementation, then I’d say doing anything an ADT approach would not lead there, something along the lines of understanding how Smalltalk, CL, and Newspeak implements their object system in memory and what advances were found in making these systems better (like a singular reflect point) would help there. If your goal is formal modeling, then your approach is fine for modeling interactions of specific objects. If this is meant to model the meta machinery around the Object model then I’d argue this may not be the most productive approach, and it may be best to wait until the implementation shows itself and we can together figure out how to do a pluggable type system here (we can add different type systems, like one for some amount of static flow control).

Depending on your specific goals it may already be useful

Yes that’s what we want. And I think, we pretty much all agree on that.