[Dev] Simplifying biref definition and kind extensions

Alec Flett alecf at osafoundation.org
Tue Sep 13 09:37:13 PDT 2005


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



More information about the Dev mailing list