1MooseX::Extended::ManuaUls:e:rTuCtoonrtirailb(u3t)ed PerMlooDsoecXu:m:eEnxttaetnidoend::Manual::Tutorial(3)
2
3
4
6 MooseX::Extended::Manual::Tutorial - Building a Better Moose
7
9 version 0.35
10
12 MooseX::Extended is built on years of experience hacking on Moose and
13 being the lead designer of the Corinna <https://github.com/Ovid/Cor>
14 project to bring modern OO to the Perl language. We love Moose, but
15 over the years, it's become clear that there are some problematic
16 design choices. Further, Corinna is not yet in core as we write this
17 (though the Perl Steering Committee has accepted it), so for now, let's
18 see how far we can push the envelope. Interestingly, in some respects,
19 MooseX::Extended offers more than the initial versions of Corinna
20 (though this won't last).
21
23 MooseX::Extended has the philosophy of providing best practices, but
24 not enforcing them. We try to make many best practices the default, but
25 you can opt out of them. For more background, see the article Common
26 Problems in Object-Oriented Code
27 <https://ovid.github.io/articles/common-problems-in-object-oriented-
28 code.html>. That's what lead to the creation of MooseX::Extended.
29
30 In particular, it's designed to make large-scale OOP systems written in
31 Moose easier to maintain by removing many common failure modes, while
32 still allowing you full control over what features you do and do not
33 want.
34
35 What follows is a fairly decent overview of MooseX::Extended. See the
36 documentation of individual modules for more information.
37
38 What's the Point.pm?
39 Let's take a look at a simple "Point" class in Moose. We want it to
40 have x/y coordinates, and the creation time as "seconds from epoch".
41 We'd also like to be able to "invert" points.
42
43 package My::Point {
44 use Moose;
45 has 'x' => ( is => 'rw', isa => 'Num', writer => 'set_x' );
46 has 'y' => ( is => 'rw', isa => 'Num', writer => 'set_y' );
47 has 'created' => ( is => 'ro', isa => 'Int', default => sub {time} );
48
49 sub invert {
50 my $self = shift;
51 my ( $x, $y ) = ( $self->x, $self->y );
52 $self->set_x($y);
53 $self->set_y($x);
54 }
55 }
56
57 1;
58
59 To the casual eye, that looks fine, but there are already many issues
60 with the above.
61
62 • The class is not immutable
63
64 You almost always want to end your Moose classes with
65 "__PACKAGE__->meta->make_immutable". Doing this causes Moose to
66 close the class definition for modifications (if that doesn't make
67 sense, don't worry about it), and speeds up the code considerably.
68
69 • Dirty namespace
70
71 Currently, "My::Point->can('has')" returns true, even though "has"
72 should not be a method. This, along with a bunch of other functions
73 exported into your class by Moose, can mislead your code and
74 confuse your method resolution order. For this reason, it's
75 generally recommended that you use "namespace::autoclean" or
76 "namespace::clean". To remove those functions from your class.
77
78 • Unknown constructor arguments
79
80 my $point = My::Point->new( X => 3, y => 4 );
81
82 In the above, the first named argument should be "x", not "X".
83 Moose simply throws away unknown constructor arguments. One way to
84 handle this might be to set your fields as "required":
85
86 has 'x' => ( is => 'rw', isa => 'Num', writer => 'set_x', required => 1 );
87 has 'x' => ( is => 'rw', isa => 'Num', writer => 'set_y', required => 1 );
88
89 That causes "My::Point->new( X => 3, y => 4 )" to throw an
90 exception, but not this: "My::Point->new( x => 3, y => 4, z => 5
91 )". For this trivial example, it's probably not a big deal, but for
92 a large codebase, where many Moose classes might have a huge
93 variety of confusing arguments, it's easy to make mistakes.
94
95 For this, we recommend MooseX::StrictConstructor. Unknown arguments
96 are fatal.
97
98 • Innappropriate constructor arguments
99
100 my $point = My::Point->new( x => 3, y => 4, created => 42 );
101
102 The above works, but the author of the class almost certainly
103 didn't intend for you to be passing "created" to the constructor,
104 but to the programmer reading the code, that's not always clear:
105
106 has 'created' => ( is => 'ro', isa => 'Int', default => sub {time} );
107
108 The fix for this is to add "init_arg => undef" to the attribute
109 definition and hope the maintenance programmer notices this:
110
111 has 'created' => ( is => 'ro', isa => 'Int', init_arg => undef, default => sub {time} );
112
113 • Misspelled types
114
115 What if "created" was defined like this?
116
117 has 'created' => ( is => 'ro', isa => 'int', default => sub {time} );
118
119 The type constraint is named "Int", not "int". You won't find out
120 about that little issue until runtime. There are a number of ways
121 of dealing with this, but we recommend the Type::Tiny family of
122 type constraints. Misspelling a type name becomes a compile-time
123 failure:
124
125 use Types::Standard 'Int';
126 has 'created' => ( is => 'ro', isa => Int, default => sub {time} );
127
128 • No signatures
129
130 Let's look at our method:
131
132 sub invert {
133 my $self = shift;
134 my ( $x, $y ) = ( $self->x, $self->y );
135 $self->set_x($y);
136 $self->set_y($x);
137 }
138
139 What if someone were to write "$point->invert( 4, 7 )"? That
140 wouldn't make any sense, but it also wouldn't throw an exception or
141 even a warning, despite it obviously not being what the programmer
142 wanted. The simplest solution is to just use signatures:
143
144 use feature 'signatures';
145 no warnings 'experimental::signatures'; # 5.34 and below
146
147 sub invert ($self) { ... }
148
149 Fixing our Moose class
150 Taking all of the above into consideration, we might rewrite our Moose
151 class as follows:
152
153 package My::Point {
154 use Moose;
155 use MooseX::StrictConstructor;
156 use Types::Standard qw(Num Int);
157 use feature 'signatures';
158 no warnings 'experimental::signatures';
159 use namespace::autoclean;
160
161 has 'x' => ( is => 'rw', isa => Num, writer => 'set_x' );
162 has 'y' => ( is => 'rw', isa => Num, writer => 'set_y' );
163 has 'created' => ( is => 'ro', isa => Int, init_arg => undef, default => sub {time} );
164
165 sub invert ($self) {
166 my ( $x, $y ) = ( $self->x, $self->y );
167 $self->set_x($y);
168 $self->set_y($x);
169 }
170
171 __PACKAGE__->meta->make_immutable;
172 }
173
174 1;
175
176 That's a lot of boilerplate for a simple x/y point class! Out of the
177 box (but almost completely customisable), MooseX::Extended provides the
178 above for you.
179
180 package My::Point {
181 use MooseX::Extended types => [qw/Num Int/];
182
183 param [ 'x', 'y' ] => ( is => 'rw', isa => Num, writer => 1 );
184 field 'created' => ( isa => Int, lazy => 0, default => sub {time} );
185
186 sub invert ($self) {
187 my ( $x, $y ) = ( $self->x, $self->y );
188 $self->set_x($y);
189 $self->set_y($x);
190 }
191 }
192
193 No need use those various modules. No need to declare the class
194 immutable or end it with a true value (MooseX::Extended does these for
195 you). Instead of remembering a bunch of boilerplate, you can focus on
196 writing your code.
197
199 In the Moose world, we use the "has" function to declare an "attribute"
200 to hold instance data for your class. This function is still available,
201 unchanged in "MooseX::Extended", but two new functions are now
202 introduced, "param" and "field", which operate similarly to "has". Both
203 of these functions default to "is => 'ro'", so that may be omitted if
204 the attribute is read-only.
205
206 A "param" is a required parameter (defaults may be used). A "field" is
207 not intended to be passed to the constructor (but see the extended
208 explanation below). This makes it much easier for a developer, either
209 writing or reading the code, to be clear about the intended class
210 interface.
211
212 So instead of this (and having the poor maintenance programmer
213 wondering what is and is not allowed in the constructor):
214
215 has name => (...);
216 has uuid => (...);
217 has id => (...);
218 has backlog => (...);
219 has auth => (...);
220 has username => (...);
221 has password => (...);
222 has cache => (...);
223 has this => (...);
224 has that => (...);
225
226 You have this:
227
228 param name => (...);
229 param backlog => (...);
230 param auth => (...);
231 param username => (...);
232 param password => (...);
233
234 field cache => (...);
235 field this => (...);
236 field that => (...);
237 field uuid => (...);
238 field id => (...);
239
240 Now the interface is much clearer.
241
242 "param"
243 param name => ( isa => NonEmptyStr );
244
245 A similar function to Moose's "has". A "param" is required. You may
246 pass it to the constructor, or use a "default" or "builder" to supply
247 this value.
248
249 The above "param" definition is equivalent to:
250
251 has name => (
252 is => 'ro',
253 isa => NonEmptyStr,
254 required => 1,
255 );
256
257 If you want a parameter that has no "default" or "builder" and can
258 optionally be passed to the constructor, just use "required => 0".
259
260 param title => ( isa => Str, required => 0 );
261
262 Note that "param", like "field", defaults to read-only, "is => 'ro'".
263 You can override this:
264
265 param name => ( is => 'rw', isa => NonEmptyStr );
266 # or
267 param name => ( is => 'rwp', isa => NonEmptyStr ); # adds _set_name
268
269 Otherwise, it behaves like "has". You can pass in any arguments that
270 "has" accepts.
271
272 # we'll make it private, but allow it to be passed to the constructor
273 # as `name`
274 param _name => ( isa => NonEmptyStr, init_arg => 'name' );
275
276 The "param"'s "is" option accepts "rwp", like Moo. It will create a
277 writer in the name "_set_${attribute_name|".
278
279 "field"
280 field cache => (
281 isa => InstanceOf ['Hash::Ordered'],
282 default => sub { Hash::Ordered->new },
283 );
284
285 A similar function to Moose's "has". A "field" is not intended to be
286 passed to the constructor, but you can still use "default" or
287 "builder", as normal.
288
289 The above "field" definition is equivalent to:
290
291 has cache => (
292 is => 'ro',
293 isa => InstanceOf['Hash::Ordered'],
294 init_arg => undef, # not allowed in the constructor
295 default => sub { Hash::Ordered->new },
296 lazy => 1,
297 );
298
299 Note that "field", like "param", defaults to read-only, "is => 'ro'".
300 You can override this:
301
302 field some_data => ( is => 'rw', isa => NonEmptyStr );
303 #
304 field some_data => ( is => 'rwp', isa => NonEmptyStr ); # adds _set_some_data
305
306 Otherwise, it behaves like "has". You can pass in any arguments that
307 "has" accepts.
308
309 The "field"'s "is" option accepts "rwp", like Moo. It will create a
310 writer in the name "_set_${attribute_name|".
311
312 If you pass "field" an "init_arg" with a defined value, the code will
313 usually throw a Moose::Exception::InvalidAttributeDefinition exception.
314 However, if the init_arg begins with an underscore, it's allowed. This
315 is designed to allow developers writing tests to supply their own
316 values more easily.
317
318 field cache => (
319 isa => InstanceOf ['Hash::Ordered'],
320 default => sub { Hash::Ordered->new },
321 init_arg => '_cache',
322 );
323
324 With the above, you can pass "_cache => $my_testing_cache" in the
325 constructor.
326
327 A "field" is automatically lazy if it has a "builder" or "default".
328 This is because there's no guarantee the code will call them, but this
329 makes it very easy for a "field" to rely on a "param" value being
330 present. It's a common problem in Moose that attribute initialization
331 order is alphabetical order and if you define an attribute whose
332 "default" or "builder" relies on another attribute, you have to
333 remember to name them correctly or declare the field as lazy.
334
335 Note that is does mean if you need a "field" to be initialized at
336 construction time, you have to take care to declare that it's not lazy:
337
338 field created => ( isa => PositiveInt, lazy => 0, default => sub {time} );
339
340 In our opinion, this tiny little nit is a fair trade-off for this
341 issue:
342
343 package Person {
344 use Moose;
345
346 has name => ( is => 'ro', required => 1 );
347 has title => ( is => 'ro', required => 0 );
348 has full_name => (
349 is => 'ro',
350 default => sub {
351 my $self = shift;
352 my $title = $self->title;
353 my $name = $self->name;
354 return defined $title ? "$title $name" : $name;
355 },
356 );
357 }
358
359 my $person = Person->new( title => 'Doctor', name => 'Who' );
360 say $person->title;
361 say $person->full_name;
362
363 The code looks fine, but it doesn't work. In the above,
364 "$person->full_name" is always undefined because attributes are
365 processed in alphabetical order, so the "full_name" default code is run
366 before "name" or "title" is set. Oops! Adding "lazy => 1" to the
367 "full_name" attribute definition is required to make it work.
368
369 Here's the same code for "MooseX::Extended". It works correctly:
370
371 package Person {
372 use MooseX::Extended;
373
374 param 'name';
375 param 'title' => ( required => 0 );
376
377 field full_name => (
378 default => sub {
379 my $self = shift;
380 my $title = $self->title;
381 my $name = $self->name;
382 return defined $title ? "$title $name" : $name;
383 },
384 );
385 }
386
387 Note that "param" is not lazy by default, but you can add "lazy => 1"
388 if you need to.
389
390 NOTE: We were sorely tempted to change attribute field definition order
391 from alphabetical to declaration order, as that would also solve the
392 above issue (and might allow for deterministic destruction), but we
393 decided to play it safe.
394
395 Attribute shortcuts
396 When using "field" or "param" (but not "has"), we have some attribute
397 shortcuts:
398
399 param name => (
400 isa => NonEmptyStr,
401 writer => 1, # set_name
402 reader => 1, # get_name
403 predicate => 1, # has_name
404 clearer => 1, # clear_name
405 builder => 1, # _build_name
406 );
407
408 sub _build_name ($self) {
409 ...
410 }
411
412 These should be self-explanatory, but see
413 MooseX::Extended::Manual::Shortcuts for a full explanation.
414
416 You may find some features to be annoying, or even cause potential bugs
417 (e.g., if you have a "croak" method, our importing of "Carp::croak"
418 will be a problem.
419
420 For example, if you wish to eliminate MooseX::StrictConstructor and the
421 "carp" and "croak" behavior:
422
423 use MooseX::Extended excludes => [qw/StrictConstructor carp/];
424
425 You can exclude the following:
426
427 • "StrictConstructor"
428
429 use MooseX::Extended::Role excludes => ['StrictConstructor'];
430
431 Excluding this will no longer import "MooseX::StrictConstructor".
432
433 • "autoclean"
434
435 use MooseX::Extended::Role excludes => ['autoclean'];
436
437 Excluding this will no longer import "namespace::autoclean".
438
439 • "c3"
440
441 use MooseX::Extended::Role excludes => ['c3'];
442
443 Excluding this will no longer apply the C3 mro.
444
445 • "carp"
446
447 use MooseX::Extended::Role excludes => ['carp'];
448
449 Excluding this will no longer import "Carp::croak" and
450 "Carp::carp".
451
452 • "immutable"
453
454 use MooseX::Extended::Role excludes => ['immutable'];
455
456 Excluding this will no longer make your class immutable.
457
458 • "true"
459
460 use MooseX::Extended::Role excludes => ['true'];
461
462 Excluding this will require your module to end in a true value.
463
464 • "param"
465
466 use MooseX::Extended::Role excludes => ['param'];
467
468 Excluding this will make the "param" function unavailable.
469
470 • "field"
471
472 use MooseX::Extended::Role excludes => ['field'];
473
474 Excluding this will make the "field" function unavailable.
475
477 We bundle MooseX::Extended::Types to make it easier to have compile-
478 time type checks, along with type checks in functions. Here's a silly
479 example:
480
481 package Not::Corinna {
482 use MooseX::Extended types => [qw(compile Num NonEmptyStr ArrayRef)];
483 use List::Util ();
484
485 # these default to 'ro' (but you can override that) and are required
486 param _name => ( isa => NonEmptyStr, init_arg => 'name' );
487 param title => ( isa => NonEmptyStr, required => 0 );
488
489 # fields must never be passed to the constructor
490 # note that ->title and ->name are guaranteed to be set before
491 # this because fields are lazy by default
492 field name => (
493 isa => NonEmptyStr,
494 default => sub ($self) {
495 my $title = $self->title;
496 my $name = $self->_name;
497 return $title ? "$title $name" : $name;
498 },
499 );
500
501 sub add ( $self, $args ) {
502 state $check = compile( ArrayRef [ Num, 1 ] );
503 ($args) = $check->($args);
504 return List;:Util::sum( $args->@* );
505 }
506 }
507
508 See MooseX::Extended::Types for more information.
509
511 After you get used to "MooseX::Extended", you might get tired of
512 exchanging the old boilerplate for new boilerplate. So don't do that.
513 Instead, create your own.
514
515 Define your own version of MooseX::Extended:
516
517 package My::Moose::Role {
518 use MooseX::Extended::Role::Custom;
519
520 sub import {
521 my ( $class, %args ) = @_;
522 MooseX::Extended::Role::Custom->create(
523 excludes => [qw/ carp /],
524 includes => ['multi'],
525 %args # you need this to allow customization of your customization
526 );
527 }
528 }
529
530 # no need for a true value
531
532 And then use it:
533
534 package Some::Class::Role {
535 use My::Moose::Role types => [qw/ArrayRef Num/];
536
537 param numbers => ( isa => ArrayRef[Num] );
538
539 multi sub foo ($self) { ... }
540 multi sub foo ($self, $bar) { ... }
541 }
542
543 See MooseX::Extended::Custom for more information.
544
546 Of course we support roles. Here's a simple role to add a "created"
547 field to your class:
548
549 package Not::Corinna::Role::Created {
550 use MooseX::Extended::Role types => ['PositiveInt'];
551
552 # mark it as non-lazy to ensure it's run at construction time
553 field created => ( isa => PositiveInt, lazy => 0, default => sub {time} );
554 }
555
556 And then consume like you would any other role:
557
558 package My::Class {
559 use MooseX::Extended types => [qw(compile Num NonEmptyStr Str PositiveInt ArrayRef)];
560
561 with qw(Not::Corinna::Role::Created);
562
563 ...
564 }
565
566 See MooseX::Extended::Role for information about what features it
567 provides and how to adjust its behavior.
568
570 For a clean Moose hierarchy, switching to MooseX::Extended is often as
571 simple at replacing Moose with MooseX::Extended and running your tests.
572 Then you can start deleting various bits of boilerplate in your code
573 (such as the "make_immutable" call).
574
575 Unfortunately, many Moose hierarchies are not clean. You might fail on
576 the "StrictConstructor", or find that you use multiple inheritance and
577 rely on dfs (depth-first search) instead of the C3 mro, or maybe
578 (horrors!), you have classes that aren't declared as immutable and you
579 have code that relies on this. A brute-force approach to handling this
580 could be the following:
581
582 package My::Moose {
583 use MooseX::Extended::Custom;
584
585 sub import {
586 my ( $class, %args ) = @_;
587 MooseX::Extended::Custom->create(
588 excludes => [qw/
589 StrictConstructor autoclean
590 c3 carp
591 immutable true
592 field param
593 /],
594 %args # you need this to pass your own import list
595 );
596 }
597 }
598 # no need for a true value
599
600 With the above, you've excluded almost everything except signatures and
601 postderef features (we will work on getting around that limitation).
602 From there, you can replace Moose with "My::Moose" (and do something
603 similar with roles) and it should just work. Then, start slowing
604 deleting the items from "excludes" until your tests fail and address
605 them one-by-one.
606
608 Moose and "MooseX::Extended" should be 100% interoperable. Let us know
609 if it's not <https://github.com/Ovid/moosex-extended/issues>.
610
612 We use GitHub Actions <https://github.com/features/actions> to run full
613 continuous integration tests on versions of Perl from v.5.20.0 and up.
614 We do not release any code that fails any of those tests.
615
617 Curtis "Ovid" Poe <curtis.poe@gmail.com>
618
620 This software is Copyright (c) 2022 by Curtis "Ovid" Poe.
621
622 This is free software, licensed under:
623
624 The Artistic License 2.0 (GPL Compatible)
625
626
627
628perl v5.36.1 2023-06-M2o6oseX::Extended::Manual::Tutorial(3)