while we're offering our feedback, +1 on this proposal. I think its going to be incredibly useful.

Alec

Phillip J. Eby wrote:
Yesterday, I posted a short and rough proposal for making it possible to define a bidirectional reference from only one class. However, discussion on IRC and some emails I got privately made it clear that I didn't really provide enough background on either the why's or how's of the proposal, and there was also some IRC discussion that led to a better solution for one of the problems, than the solution I proposed here yesterday. So, I'm going to restate the proposal to incorporate that enhancement, and also to provide some of the background that was asked for.

Donn asked, "why is circularity a problem?" and "why is it more of a problem now?" And the answer to both questions is that circularity breaks modularity. Because if component A depends on component B, and B depends on A, then you can't use either one without the other, and so you no longer have any meaningful distinction between A and B - they might as well be the same component. You lose the ability for someone to learn A and then B, and you lose the ability to have A first and then optionally add B later.

So, the problem is that bidirectional reference definitions being split across parcels breaks this modularity, and the problem is popping up more now as we try to enforce the modularization of the parcel structure. We still want to have bidirectional references across parcels, of course, but we need to be able to define them without making A depend on B and B depend on A. We'd like to be able to define the whole biref from *one* parcel, so that you can have A and then add B later, and if you never add B then A still works as-is.

Right now, however, if you define a biref with the schema API, you have to do half of it in A, and half in B. This is fundamentally broken because it means you can't have *any* modularity and still relate things in different parcels. So, we need a way to define both sides of a biref from only one place.

And that's the first part of my proposal, that we allow you to define a biref from only one "side". In many of the cases of birefs in our current schema, the other side is only there because we *have to have it*; we never actually use the "A" side of the biref, we're only really using the "B" side.

Let's take Morgen's sharing use case as an example. The sharing parcel needs to keep a collection mapping iCal UID's to calendar events. In this case, calendar events are part of parcel "A" - the "pim" parcel. The pim parcel shouldn't have anything to do with sharing, or else it can't be used independently, or taught independently. (That is, if you have to understand sharing before you can fully understand the pim parcel, we have a learning curve problem as well as an inability to deploy them separately.)

But, the only way Morgen can have a bidirectional reference between the sharing parcel and calendar events, is if he *modifies* the calendar event mixin class to add an attribute, which then makes the calendar parcel depend on sharing - making A depend on B, in other words. This approach doesn't scale very well, and it definitely doesn't work for third-party parcels. And at our current team and application size, we are starting to run into problems because effectively we are all "third-party" with respect to one another's code. That is, in this example, Morgen is "third-party" when it comes to the calendar parcel.

So the first part of what I'm proposing, then, is that in the sharing parcel, Morgen should be able to do this:

    items = schema.Sequence(pim.CalendarEventMixin, inverse=schema.One())

and *not* have to go edit the CalendarEventMixin class, just to add the backward reference that he never uses anyway. He just specifies that he wants a new 'One()' reference to be added to the CalendarEventMixin kind in the repository, and this will happen as soon as his parcel is installed. The calendar parcel, meanwhile, can be loaded and used *without* the sharing parcel, because it doesn't have any references to sharing defined in its code. The calendar developers don't have to ask, "what's this sharing thing in our code?", and so they are happy. Morgen doesn't have to worry about annoying the calendar developers, or what to call the extra attribute he doesn't want anyway, and so Morgen is happy. Life is good. :)

There's an additional detail to this idea, which is how it's implemented internally. When you create a "one-way biref" like this, it will actually add a new attribute to CalendarEventMixin for you. You just don't have to give it a name, or add it to the class by hand. The name this attribute will be automatically given is "osaf.sharing.UIDMap.items.inverse", which of course cannot collide with any of the calendar-specific attributes defined by the calendar parcel. It does mean that it's more awkward to access that attribute, if you really need to access it for some reason, because you have to use getattr(ob,name) or ob.getAttributeValue(name) (where 'name' is "osaf.sharing.UIDMap.items.inverse"). You can't just say 'ob.name' the way you can with attributes that are created explicitly.

This is a feature, though, not a bug. The fact that you can't access it via 'ob.name' means that the calendar parcel can never *accidentally* use this attribute, or define a conflicting attribute. This is a good thing, because it means that no matter what other parcels do to the kind, the calendar parcel never needs to know about it. It can define whatever attributes it wants, and everybody else can have whatever attributes they want, and everybody is happy. Life is still good. :)

Okay, so what about the case where you really want to be able to use that attribute? Or what if you just want to add an attribute to an existing kind, like in the AbstractCollection.color case?

Well, that's what the second proposed feature is for, and this part of the proposal is a bit different today, based on the IRC discussions yesterday. It's an API to allow you to define these additional attributes, and to access them conveniently, without having to spell out attribute names like "osaf.sharing.UIDMap.items.inverse". Here's an example, loosely based on a suggestion by Alec on IRC yesterday:

    class SidebarInfo(schema.Annotation):
        schema.annotates(pim.AbstractCollection)
        calendarColor = schema.One(blocks.ColorType)
        alertSound    = schema.One(schema.Lob)

If this class were defined in "some_module", then loading that module into the repository would add two new attributes to the AbstractCollection kind: "some_module.SidebarInfo.calendarColor", and "some_module.SidebarInfo.alertSound".

But, it also does one other thing, which makes it much more useful. The SidebarInfo class is actually an "annotation wrapper" class that you can apply to an item, in order to access the attributes "normally". That is, the Annotation subclass would have automatically-defined properties that look up the corresponding attributes on an underlying item.

So, if you wanted to get the calendar color of a collection, you would do this:

    the_color = SidebarInfo(some_collection).calendarColor

And if you wanted to set a collection's calendar color, you would do this:

    SidebarInfo(some_collection).calendarColor = the_color

And in each case, the attribute being get or set on the annotation object would cause the attribute to be get or set (using its full, dotted, internal name) on the wrapped item.

If you are doing lots of things with a particular annotation, you can of course save it in a variable, and use it more than once:

   sbi = SideBarInfo(some_collection)
MessageBox(("Your color is %s" % sbi.calendarColor), sound=sbi.alertSound)

However, annotation wrappers aren't persistent and shouldn't be stored in the repository -- although they could be later if we have the need. They're really just a convenience for Python code, at the moment, though, and things like attribute editors should probably just use the attributes' full dotted names, rather than using a wrapper to access them.

In addition to annotation attributes, you can also define methods on Annotation classes, and then use these methods on the instances, e.g.:

    class SidebarInfo(schema.Annotation):

        schema.annotates(pim.AbstractCollection)

        calendarColor = schema.One(blocks.ColorType)
        alertSound    = schema.One(schema.Lob)

        def alert(self):
            MessageBox(
                 ("Your color is %s" % self.calendarColor),
                 sound = self.alertSound
             )

    # Alert about some_collection:
    SidebarInfo(some_collection).alert()

Thus, you get a kind of "dynamic mixin" capability that's ideal for adding extra information and behavior needed by "third party" parcels. (Except that third party is a misleading name, since most of our parcels are "third party" relative to some other parcel).

There are a couple more examples I need to present, in order to show how the two proposals above (i.e. "one-way" birefs and annotation classes) work together. First, I'll revisit yesterday's Contact likers/likees example:

    class Friends(schema.Annotation):
        schema.annotates(pim.Contact)
        likes = schema.Many(pim.Contact)
        isLikedBy = schema.Many(pim.Contact, inverse=likes)

    Friends(somebody).likes       # get the contacts who somebody likes
    Friends(somebody).isLikedBy   # get the contacts who like somebody
    you in Friends(me).isLikedBy  # do you like me?
    me in Friends(you).likes      # no, really, do you like me?  :)

    Friends(everybody).likes.add(somebody)  # everybody likes somebody!
Friends(me).likes.remove(you) # I don't like you any more :(

This of course is the special case where both attributes are annotating the same existing kind. If we wanted to create a biref between two different existing kinds, we might have something like:

    class Favorites(schema.Annotation):

        schema.annotates(pim.Contact)

        favorite_feeds = schema.Many(feeds.Feed)
        favorite_movies = schema.Many(movies.Movie)

        # ... other 'favorite things' attributes here


    class FavoriteFeed(schema.Annotation):

        schema.annotates(feeds.Feed)

        favorite_of = schema.Many(
            pim.Contact, inverse=Favorites.favorite_feeds
        )

We could then use 'FavoriteFeed(some_feed).favorite_of' to find the people who consider 'some_feed' a favorite, and we can use 'Favorites(some_contact).favorite_feeds' to find a person's favorite feeds. (And we can do all this without modifying either the pim or feeds parcels.)

The last example covers the case where a parcel wants to create a two-way link between an existing kind and a new kind:

    class SoccerMatch(pim.ContentItem):
        # ... various other attributes here
        referee = schema.One(pim.Contact)
        # ... more attributes here

    class SoccerReferee(schema.Annotation):
        schema.annotates(pim.Contact)
        refereed_games = schema.Sequence(
            SoccerMatch, inverse=SoccerMatch.referee
        )

We can now use some_match.referee to find a match's referee, and we can find out if a contact has refereed any games using 'SoccerReferee(some_contact).refereed_games'. We could also add methods to the SoccerReferee class to do things like compute statistics about the refereed games, etc.

Now, you could make an argument that this last use case should be implemented by creating a SoccerReferee kind, and I wouldn't necessarily disagree with you. However, as the number of roles an individual plays increases, the number of mixin kinds is O(2^N). That is, every time a mixin is added, the total number of kinds doubles. Having just three mixins means eight kinds (what we have now for stamping), 4 mixins means 16 kinds, and by the time you get to twenty mixins there are over a million potential kinds. That's an awful lot of repository space just to store all the different kind mixtures. :)

The annotation approach, on the other hand, doesn't create any new kinds, but instead allows items to be of multiple "virtual" kinds at once. As Donn pointed out in an email this morning, this means that annotations might end up being a better way to implement extensible stamping in future versions of Chandler.

Anyway, that's the updated proposal.  Comments?  Questions?

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev

Reply via email to