KBOS attributes
Welcome to the fifth post about the Kephra Base Object System, where I explain the need for three kinds of attributes: data, delegating and wrapping and gas a little about their properties. It is especially advised to have read the first part (scopes) and the previous part, since accessors are methods.
In case you wonder how practical this exercise is - I already implemented a slightly simpler version and currently rewrite it. It's a standalone bundle of modules with its own tests and docs named Base-Class (Kephra::Base::Class) (123kB), which depends only on the bundle Base (there are the data types) (32 kB). So once ready it could be released on CPAN without much work.
Perspective
I also plan an addendum post to this series, where I sum up my last two YAPC lightning talks on the topic: how to proper OO. In short: objects are better not seen as data storing things, but behaviour. Nonetheless they have a state and a proper OO system should make it easy to access this data. Not just for marshalling, but also for debugging and other introspection purposes. KBOS does that by auto generating a pair of methods: state and restate. One gives you a complete data dump (like method .perl in Raku) and the other can recreate from the result of state a new the object as if you did clone the original (would be a multi of new in Raku).
Three Kinds
Once you see where I come from, than the rest is mere consequence. Normal attributes contain data, that can be serialized by Sereal or YAML, hence I call them data attributes. You just create an hash {attr_name => attr_data, ...} and dump it. Some attributes are KBOS objects themself. They are called delegating because of the mechanism that delegates method calls of the mother object to methods of the attribute (more on that later). Their attr_data is in itself such a hash, delivered by its own state method. But what do I do with none KBOS objects? I put them in wrapping attributes. There I can define a set of virtual subattributes. To each subattribute I have to write a piece of code, that fetches the data from the none-KBOS object. It is your duty as a class author to make sure that the virtual subattributes describe the full state of the object. KBOS ensures that all subattributes are either used as named arguments of the default constructor or have also associated code snippets to recreate the state by method call on the none-KBOS object with one or more subattributes as arguments.
Syntax
You declare a KBOS attribute:
delegating attribute name => { .... };
Instead of delegating you could also write wrapping or data (optional) - you just say what you want. Inside the hashref are placed the attribute properties. The only one all three kinds share is help (required). You have to specify what this attribute holds/stands for. I need this for good error messages and it makes the code literally self documenting. The next property is type, which reads much more descriptive than isa. wrapping and delegating attributes have class instead of type.
Data Accessors and Builders
As a rule of thumb: KBOS is simple. Either you do the magic inside a self implemented accessor and tell KBOS later that this is the getter/settter/whatever of the attribute or you autogenerate such method (by choosing a getter name without implementation). This getter will type check and foreward incoming values. Just write getter => method_name (same for setter or builder). KBOS checks if the method was declared as a getter, but has no way to verify if the method is a getter or setter - these names are for clarity and convenience).
Inside the getter you will get an accessor object to the attribute, as described last time. If you want the getter to be private just say so: getter => {name => ...., scope => private}. Other scopes are ACCESS (tells you the getter/setter is only used implicitly by other getter/setter and not in (normal) public and private methods) and BUILD (tells you this setter is only used by a constructor - it's a readonly attribute - which you also get if you have no setter at all and set value in constructor). Once you get the hang of it to see the attributes purpose in its scope declaration, you see this system is clearer than Raku/Moose and much more flexible. If you want a combined getter/setter - easy - just write getter => samename, setter => samename. They can be even differently scoped, so you have a combined getter/setter privately, but publicly just a getter. There is also no nonsense like a required attribute, since that reaches into the domain of the constructors signature, which you declare of course on the constructor - tadzik would be happy. But of course we do have argument builder, that are called implicitly after the constructor - even lazy ones, that are called implicitly on first call of the getter: builder => {lazy => 1, name => buildermethod} or builder => {lazy => 1, value => 23}. Goes without saying builder methods are BUILD scoped, meaning only callable inside a constructor or destructor method or by internal core methods of KBOS. To keep private attributes private, state and restate are also BUILD scoped. Unlike Raku and other (Moos'es) we don't have a special BUILD or BUILDARGS method. You do your BUILDARGS work inside of the constructor or the BUILD scoped setter/builder. restate has to be in BUILD scope too, so you have the ability to prevent any bypassing of your BUILDARGS phase by ordinary users calling restate.
Wrapping Foreign Objects
What I wrote so far is all about data attributes. With delegating attributes you don't have any getter or setter, you just have builder and delegator (these get the attribute (KBOS object in public scope) to make calls on it). Same principles apply as descibed - you can just map public methods from attribute onto the mother class (with or without rename) or you write your magical delegator methods.
Wrapper attributes have analogously the properties wrapper and builder. The already described subattributes are declared with: var => { position => { get => '$self->GetPos'}, set => '$self->SetPos( $var->{'position'}, help => 'vertical or horizontal splitter position' )'}. The vars a.k.a virtual attributes serve the same purpose as the attributes of a delegating attribute. When fetched from an active object they build together a hash of attribute values, like a KBOS object would do. Inside the set snippets you access that hash directly via $var. If you leave out the code snippe on the set property, than only because your constructor is: new (Int position) {.....} , which allows KBOS to foreward $var->{'position'} to the constructors argument, hence this virtual attribute gets marshalled too.
Even this concept is still rough on the edges, I wanted to present it - I'm very much interested in discussion. Next time will be about actual object usage and some internal design details. After that - only the mentioned addendum.
Leave a comment