This is part eight in the "Cases of UPPER" series of blog posts, describing the Raku syntax elements that are completely in UPPERCASE.
This part will discuss the interface methods that one can provide to tweak the creation of objects in the Raku Programming Language.
But first some background
If you are unfamiliar as to how Raku allows one to create classes, describe attributes and methods, it is recommended to first have a look at Classes and objects - A tutorial about creating and using classes in Raku.
In short: by default, objects are created with the .new method (which is provided by the Mu base class). This .new method takes named arguments and tries to match these with attribute names that are supposed to be set with .new. Any unmatched named arguments are silently ignored.
Attributes that are settable with .new that are not specified will have any default value applied to them. A small example:
class Foo {
has $.bar = 42; # attribute + accessor
}
say Foo.new.bar; # 42
say Foo.new( :bar(666) ).bar; # 666
Well, actually that is a little white lie. But it is true in most cases.
The thing is that you can also specify your own .new method in your class, and still have all of the attribute initialization work done for you. The reason for this is that it's not really the .new method that is doing the work, but the .bless method that is also provided by the Mu class.
Functionally the .new method supplied by Mu is:
method new() { self.bless(|%_) }
although in reality it's a little bit more complex more allowing for some optimizations. Object creation occurs a lot when you're executing a Raku program, so it makes sense to try to optimize that!
Note that methods in Raku always have a
*%_(aka a "slurpy hash") added to their signature, unless there is already a slurpy hash in the signature.
An example of a custom .new method that takes a single positional argument:
class Foo {
has $.bar = 42;
multi method new($bar) {
self.bless(:$bar, |%_)
}
}
say Foo.new.bar; # 42
say Foo.new( :bar(666) ).bar; # 666
say Foo.new( 137 ).bar; # 137
The reason that this also allows the named argument way, is because of the multi. This adds a candidate to the existing dispatch table for the .new method, so the Mu.new candidate can still be dispatched to. Without the multi there would only be the one .new method in the dispatch table for class Foo.
Also note the |%_ in the self.bless call: this makes sure that any additional named argments are also passed to .bless if you specify a positional argument.
BUILD vs TWEAK
Any class in Raku can have a BUILD and/or a TWEAK method specified. Both are called by the object building logic, but only if it is possible to call them. Both methods receive the same arguments as having been (indirectly) passed to .bless. The return value of these methods will be ignored.
The BUILD method is called instead of attribute initializations from the named arguments. Specifying the BUILD method means needing to mimic all named argument setting in that BUILD method..
Any default values for attributes are assigned if the attribute did not receive a value yet (either from attribute initializations from the named arguments, or having been initialized in the BUILD method).
The TWEAK method is called as the last stage of object instantiation. Nowadays it is the recommended way of altering the named argument -> attribute initialization logic for a class (after all initializations and default setting has been done).
BUILDPLAN
The Rakudo distribution comes with a handy introspection module that shows what steps are taken (in pseudo-code) in the creation of an instance of a class: BUILDPLAN. It's use is pretty simple: use BUILDPLAN class.
class Foo {
has $.bar = 42;
}
use BUILDPLAN Foo;
will show:
class Foo BUILDPLAN:
0 nqp::getattr(obj,Foo,'$!bar') = :$bar if possible
1 nqp::getattr(obj,Foo,'$!bar') = 42 if not set
Note that there are two steps:
- assign value of named argument "bar" to the attribute "$!bar" if specified
- assign value
42to the "$!bar" attribute if it has not been set yet
Now, if we add a BUILD method to it:
class Foo {
has $.bar = 42;
}
submethod BUILD() { }
use BUILDPLAN Foo;
the build plan changes to:
class Foo BUILDPLAN:
0 call obj.Foo::BUILD
1 nqp::getattr(obj,Foo,'$!bar') = 42 if not set
Note that the first step changed from "nqp::getattr(obj,Foo,'$!bar') = :$bar if possible" to "call obj.Foo::BUILD". So instead of looking for a named argument "bar", it is now just calling the "BUILD" method.
Also note that second step stayed the same: so if the BUILD method doesn't assign anything to the $!bar attribute, the default value will still be set.
Tricks and Tips
Historically the BUILD method was one of the first things actually implemented in Raku, and thus a lot of (older) code in the wild uses the BUILD method. Since then many features have been added to the way one can specify attributes, obsoleting some of the common BUILD uses.
Here are some examples of outdated idioms:
Making a named argument required
The BUILD method can make a named argument for the creation of the object mandatory by making it a required named argument of BUILD (by adding a ! in the signature):
class Foo {
has $.bar;
submethod BUILD( :$bar! ) { $!bar = $bar }
}
The better way is to use the is required trait on attributes.
class Foo {
has $.bar is required;
}
Allowing a named argument without automatic accessor
By allowing a named argument in the BUILD signature and assigning that to the attribute in the body, you're effectively mimicing the . twigil in the attribute definition without having an accessor made automatically.
class Foo {
has $!bar;
submethod BUILD( :$bar ) { $!bar = $bar }
}
The better way is to use the is built attribute trait:
class Foo {
has $!bar is built;
}
When to use TWEAK
Looking at the modules in the ecosystem, the TWEAK method is oftentimes being used for:
- throwing an error if some complicate condition is not met
- throwing an error if conflicting arguments have been specified
- starting async workers once the object is fully fleshed
- setting up a (fast) data-structure based on the attributes supplied
- setting attributes from information supplied in a (JSON) hash
- reading / parsing a file into additional attributes
- inform a logger when an object has been created
You typically should not use a TWEAK method if you can achieve the same effect with an extensive specification of the attributes. First of all the declarative manner in which these are specified is easier to comprehend. Secondly, the processing of the named arguments and their specificaton is highly optimized. Compare:
$ raku -e 'class A { has $.a = 42 }; A.new for ^1000000; say now - ENTER now'
0.080668357
$ raku -e 'class A { has $.a; method TWEAK(:$a) { $!a = $a // 42 }; A.new for ^1000000; say now - ENTER now'
0.122443318
which shows that using proper attribute declarations is at least 30% faster. Well in this case. YMMV.
If you're more comfortable with using a TWEAK method, then please do. There's more than one way to do it!
submethod vs method
A submethod is a special type of public method, but which is not inherited by subclasses. TWEAK and BUILD methods should be made submethods, because otherwise they can get executed more than once if they are in a base class, and the inheriting class does not specify its own TWEAK or BUILD method.
class A {
method TWEAK() { say "A" }
}
class B is A { }
B.new;
will show:
A
A
because class "B" inherited the TWEAK method from "A". And at object initialization, the BUILDPLAN of each class in the class hierarchy is executed.
Conclusion
This concludes the eight episode of cases of UPPER language elements in the Raku Programming Language, the first discussion interface methods.
In this episode the TWEAK and BUILD methods were described, with a supporting role for the BUILDPLAN module. In short: don't use BUILD if you can use TWEAK. Don't use TWEAK if you can describe the same logic in the attribute specifications.
Stay tuned for the next episode!
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.