/
Mooish.pm6
executable file
·1089 lines (883 loc) · 39.1 KB
/
Mooish.pm6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
unit module AttrX::Mooish:ver<0.6.902>:auth<github:vrurg>;
#use Data::Dump;
use nqp;
=begin pod
=head1 NAME
C<AttrX::Mooish> - extend attributes with ideas from Moo/Moose (laziness!)
=head1 SYNOPSIS
use AttrX::Mooish;
class Foo {
has $.bar1 is mooish(:lazy, :clearer, :predicate) is rw;
has $!bar2 is mooish(:lazy, :clearer, :predicate, :trigger);
has Num $.bar3 is rw is mooish(:lazy, :filter);
method build-bar1 {
"lazy init value"
}
method !build-bar2 {
"this is private mana!"
}
method !trigger-bar2 ( $value ) {
# do something after attribute changed.
}
method build-bar3 {
rand;
}
method filter-bar3 ( $value, *%params ) {
if %params<old-value>:exists {
# Only allow the value to grow
return ( !%params<old-value>.defined || $value > %params<old-value> ) ?? $value !! %params<old-value>;
}
# Only allow inital values from 0.5 and higher
return $value < 0.5 ?? Nil !! $value;
}
method baz {
# Yes, works with private too! Isn't it magical? ;)
"Take a look at the magic: «{ $!bar2 }»";
}
}
my $foo = Foo.new;
say $foo.bar1;
say $foo.bar3.defined ?? "DEF" !! "UNDEF";
for 1..10 { $foo.bar3 = rand; say $foo.bar3 }
The above would generate a output similar to the following:
lazy init value
UNDEF
0.08662089602505263
0.49049512098324255
0.49049512098324255
0.5983833081770437
0.9367804461546302
0.9367804461546302
0.9367804461546302
0.9367804461546302
0.9367804461546302
0.9367804461546302
=head1 DESCRIPTION
This module is aiming at providing some functionality we're all missing from Moo/Moose. It implements laziness,
accompanying methods and adds attribute value filter on top of what standard Moo/Moose provide.
What makes this module different from previous versions one could find in the Perl6 modules repository is that it
implements true laziness allowing I<Nil> to be a first-class value of a lazy attribute. In other words, if you look at
the L<#SYNOPSIS> section, C<$.bar3> value could randomly be either undefined or 3.1415926.
=head2 Laziness for beginners
This section is inteded for beginners and could be skipped by experienced lazybones.
=head3 What is "lazy attribute"
As always, more information could be found by Google. In few simple words: a lazy attribute is the one which gets its
first value on demand, i.e. – on first read operation. Consider the following code:
class Foo {
has $.bar is mooish(:lazy, :predicate);
method build-bar { π }
}
my $foo = Foo.new
say $foo.has-bar; # False
say $foo.bar; # 3.1415926...
say $foo.has-bar; # True
=head3 When is it useful?
Laziness becomes very handy in cases where intializing an attribute is very expensive operation yet it is not certain
if attribute is gonna be used later or not. For example, imagine a monitoring code which raises an alert when a failure
is detected:
class Monitor {
has $.notifier;
has $!failed-object;
submethod BUILD {
$!notifier = Notifier.new;
}
method report-failure {
$.notifier.alert( :$!failed-object );
}
...
}
Now, imagine that notifier is a memory-consuming object, which is capable of sending notification over different kinds
of media (SMTP, SMS, messengers, etc...). Besides, preparing handlers for all those media takes time. Yet, failures are
rare and we may need the object, say, once in 10000 times. So, here is the solution:
class Monitor {
has $.notifier is mooish(:lazy);
has $!failed-object;
method build-notifier { Notifier.new( :$!failed-object ) }
method report-failure {
$.notifier.alert;
}
...
}
Now, it would only be created when we really need it.
Such approach also works well in interactive code where many wuch objects are created only the moment a user action
requires them. This way overall responsiveness of a program could be significally incresed so that instead of waiting
long once a user would experience many short delays which sometimes are even hard to impossible to be aware of.
Laziness has another interesting application in the area of taking care of attribute dependency. Say, C<$.bar1> value
depend on C<$.bar2>, which, in turn, depends either on C<$.bar3> or C<$.bar4>. In this case instead of manually defining
the order of initialization in a C<BUILD> submethod, we just have the following code in our attribute builders:
method build-bar2 {
if $some-condition {
return self.prepare( $.bar3 );
}
self.prepare( $.bar4 );
}
This module would take care of the rest.
=head1 USAGE
The L<#SYNOPSIS> is a very good example of how to use the trait C<mooish>.
=head2 Trait parameters
=begin item
I<C<lazy>>
C<Bool>, defines wether attribute is lazy. Can have C<Bool>, C<Str>, or C<Callable> value. The later two have the
same meaning, as for I<C<builder>> parameter.
=end item
=begin item
I<C<builder>>
Defines builder method for a lazy attribute. The value returned by the method will be used to initialize the attribute.
This parameter can have C<Str> or C<Callable> values or be not defined at all. In the latter case we expect a method
with a name composed of "I<build->" prefix followed by attribute name to be defined in our class. For example, for a
attribute named C<$!bar> the method name is expected to be I<build-bar>.
A string value defines builder's method name.
A callable value is used as-is and invoked as an object method. For example:
class Foo {
has $.bar is mooish(:lazy, :builder( -> $,*% {"in-place"} );
}
$inst = Foo.new;
say $inst.bar;
This would output 'I<in-place>'.
*Note* the use of slurpy C<*%> in the pointy block. Read about callback parameters below.
=end item
=begin item
I<C<predicate>>
Could be C<Bool> or C<Str>. When defined trait will add a method to determine if attribute is set or not. Note that
it doesn't matter wether it was set with a builder or by an assignment.
If parameter is C<Bool> I<True> then method name is made of attribute name prefixed with U<has->. See
L<#What is "lazy attribute"> section for example.
If parameter is C<Str> then the string contains predicate method name:
=begin code
has $.bar is mooish(:lazy, :predicate<bar-is-ready>);
...
method baz {
if self.bar-is-ready {
...
}
}
=end code
=end item
=begin item
I<C<clearer>>
Could be C<Bool> or C<Str>. When defined trait will add a method to reset the attribute to uninitialzed state. This is
not equivalent to I<undefined> because, as was stated above, I<Nil> is a valid value of initialized attribute.
Similarly to I<C<predicate>>, when I<True> the method name is formed with U<clear-> prefix followed by attribute's name.
A C<Str> value defines method name:
=begin code
has $.bar is mooish(:lazy, :clearer<reset-bar>, :predicate);
...
method baz {
$.bar = "a value";
say self.has-bar; # True
self.reset-bar;
say self.has-bar; # False
}
=end code
=end item
=begin item
I<C<filter>>
A filter is a method which is executed right before storing a value to an attribute. What is returned by the method
will actually be stored into the attribute. This allows us to manipulate with a user-supplied value in any necessary
way.
The parameter can have values of C<Bool>, C<Str>, C<Callable>. All values are treated similarly to the C<builder>
parameter except that prefix 'I<filter->' is used when value is I<True>.
The filter method is passed with user-supplied value and two named parameters: C<attribute> with full attribute name;
and optional C<old-value> which could omitted if attribute has not been initialized yet. Otherwise C<old-value> contains
attribute value before the assignment.
B<Note> that it is not recommended for a filter method to use the corresponding attribute directly as it may cause
unforseen side-effects like deep recursion. The C<old-value> parameter is the right way to do it.
=end item
=begin item
I<C<trigger>>
A trigger is a method which is executed when a value is being written into an attribute. It gets passed with the stored
value as first positional parameter and named parameter C<attribute> with full attribute name. Allowed values for this
parameter are C<Bool>, C<Str>, C<Callable>. All values are treated similarly to the C<builder> parameter except that
prefix 'I<trigger->' is used when value is I<True>.
Trigger method is being executed right after changing the attribute value. If there is a C<filter> defined for the
attribute then value will be the filtered one, not the initial.
=end item
=begin item
I<C<alias>, C<aliases>, C<init-arg>, C<init-args>>
Those are four different names for the same parameter which allows defining attribute aliases. So, whereas Internally
you would have single container for an attribute that container would be accessible via different names. And it means
not only attribute accessors but also clearer and predicate methods:
class Foo {
has $.bar is rw is mooish(:clearer, :lazy, :aliases<fubar baz>);
method build-bar { "The Answer" }
}
my $inst = Foo.new( fubar => 42 );
say $inst.bar; # 42
$inst.clear-baz;
say $inst.bar; # The Answer
$inst.fubar = pi;
say $inst.baz; # 3.1415926
Aliases are not applicable to methods called by the module like builders, triggers, etc.
=end item
=begin item
I<C<no-init>>
This parameter will prevent the attribute from being initialized by the constructor:
class Foo {
has $.bar is mooish(:lazy, :no-init);
method build-bar { 42 }
}
my $inst = Foo.new( bar => "wrong answer" );
note $inst.bar; # 42
=end item
=begin item
I<C<composer>>
This is a very specific option mostly useful until role C<COMPOSE> phaser is implemented. Method of this option is
called upon class composition time.
=end item
=head2 Public/Private
For all the trait parameters, if it is applied to a private attribute then all auto-generated methods will be private
too.
The call-back style options such as C<builder>, C<trigger>, C<filter> are expected to share the privace mode of their
respective attribute:
=begin code
class Foo {
has $!bar is rw is mooish(:lazy, :clearer<reset-bar>, :predicate, :filter<wrap-filter>);
method !build-bar { "a private value" }
method baz {
if self!has-bar {
self!reset-bar;
}
}
method !wrap-filter ( $value, :$attribute ) {
"filtered $attribute: ($value)"
}
}
=end code
Though if a callback option is defined with method name instead of C<Bool> I<True> then if method wit the same privacy
mode is not found then opposite mode would be tried before failing:
=begin code
class Foo {
has $.bar is mooish( :trigger<on_change> );
has $!baz is mooish( :trigger<on_change> );
has $!fubar is mooish( :lazy<set-fubar> );
method !on_change ( $val ) { say "changed! ({$val})"; }
method set-baz { $!baz = "new pvt" }
method use-fubar { $!fubar }
}
$inst = Foo.new;
$inst.bar = "new"; # changed! (new)
$inst.set-baz; # changed! (new pvt)
$inst.use-fubar; # Dies with "No such private method '!set-fubar' for invocant of type 'Foo'" message
=end code
=head2 User method's (callbacks) options
User defined (callback-type) methods receive additional named parameters (options) to help them understand their
context. For example, a class might have a couple of attributes for which it's ok to have same trigger method if only it
knows what attribute it is applied to:
=begin code
class Foo {
has $.foo is rw is mooish(:trigger('on_fubar'));
has $.bar is rw is mooish(:trigger('on_fubar'));
method on_fubar ( $value, *%opt ) {
say "Triggered for {%opt<attribute>} with {$value}";
}
}
my $inst = Foo.new;
$inst.foo = "ABC";
$inst.bar = "123";
=end code
The expected output would be:
=begin code
Triggered for $!foo with with ABC
Triggered for $!bar with with 123
=end code
B<NOTE:> If a method doesn't care about named parameters it may only have positional arguments in its signature. This
doesn't work for pointy blocks where anonymous slurpy hash would be required:
=begin code
class Foo {
has $.bar is rw is mooish(:trigger(-> $, $val, *% {...}));
}
=end code
=head3 Options
=begin item
I<C<attribute>>
Full attribute name with twigil. Passed to all callbacks.
=end item
=begin item
I<C<builder>>
Only set to I<True> for C<filter> and C<trigger> methods when attribute value is generated by lazy builder. Otherwise no
this parameter is not passed to the method.
=end item
=begin item
I<C<old-value>>
Set for C<filter> only. See its description above.
=end item
=head2 Some magic
Note that use of this trait doesn't change attribute accessors. More than that, accessors are not required for private
attributes. Consider the C<$!bar2> attribute from L<#SYNOPSIS>.
=head2 Performance
Module versions prior to v0.5.0 were pretty much costly perfomance-wise. This was happening due to use of C<Proxy> to
handle all attribute read/writes. Since v0.5.0 only the first read/write operation would be handled by this module
unless C<filter> or C<trigger> parameters are used. When C<AttrX::Mooish> is assured that the attribute is properly
initialized it steps aside and lets the Perl6 core to do its job without intervention.
The only exception takes place if C<clearer> parameter is used and C«clear-<attribute>» method is called. In this case
the attribute state is reverted back to uninitialized state and C<Proxy> is getting installed again – until the next
read/write operation.
C<filter> and C<trigger> are exceptional here because they require permanent monitoring of attribute operations making
it effectively impossible to drop C<Proxy>. For this reason use of these parameters must be very carefully considered
and highly discouraged for any code where performance is of the high precedence.
=head1 CAVEATS
Due to the magical nature of attribute behaviour conflicts with other traits are possible. None is known to the author
yet.
Internally C<Proxy> is used as attribute container. It was told that the class has a number of unpleasant side effects
including multiplication of FETCH operation. Though generally this bug is harmles it could be workarounded by assigning
an attribute value to a temporary variable.
=head1 AUTHOR
Vadim Belman <vrurg@cpan.org>
=head1 LICENSE
Artistic License 2.0
See the LICENSE file in this distribution.
=end pod
class X::Fatal is Exception {
#has Str $.message is rw;
}
class X::TypeCheck::MooishOption is X::TypeCheck {
method expectedn {
"Str or Callable";
}
}
my %attr-data;
# PvtMode enum defines what privacy mode is used when looking for an option method:
# force: makes the method always private
# never: makes it always public
# as-attr: makes is strictly same as attribute privacy
# auto: when options is defined with method name string then uses attribute mode first; and uses opposite if not
# found. Always uses attribute mode if defined as Bool
enum PvtMode <pvmForce pvmNever pvmAsAttr pvmAuto>;
role AttrXMooishClassHOW { ... }
role AttrXMooishHelper {
method setup-helpers ( Mu \type, $attr ) is hidden-from-backtrace {
# note "SETUP HELPERS ON ", type.^name, " // ", type.HOW.^name;
# note " .. for attr ", $attr.name;
my %helpers =
:clearer( my method {
my $obj-id = self.WHICH;
# Can't use $attr to call bind-proxy upon if the original attribute belongs to a role. In this case it's
# .package is not defined.
# Metamodel::GenericHOW only happens for role attributes
# note "THIS IS CLEARER for {$attr.name}";
my $attr-obj = $attr.package.HOW ~~ Metamodel::GenericHOW ??
(
( try { self.^get_attribute_for_usage($attr.name) } )
|| self.^attributes.grep({ $_.name eq $attr.name }).first
)
!! $attr;
$attr-obj.bind-proxy( self, $obj-id );
$attr.clear-attr( $obj-id )
} ),
:predicate( my method { $attr.is-set( self.WHICH ) } ),
;
my @aliases = $attr.base-name, |$attr.init-args;
for %helpers.keys -> $helper {
next unless $attr."$helper"(); # Don't generate if attribute isn't set
#note "op2method for helper $helper";
for @aliases -> $base-name {
my $helper-name = $attr.opt2method( $helper, :$base-name );
X::Fatal.new( message => "Cannot install {$helper} {$helper-name}: method already defined").throw
if type.^declares_method( $helper-name );
my $m = %helpers{$helper};
$m.set_name( $helper-name );
#note "Installing helper $helper $helper-name on {type.^name} // {$m.WHICH}";
#note "HELPER:", %helpers{$helper}.name, " // ", $m.^can("CALL-ME"), " // ", $m.^name;
if $attr.has_accessor { # I.e. – public?
#note ". Installing public $helper-name";
type.^add_method( $helper-name, $m );
} else {
#note "! Installing private $helper-name";
type.^add_private_method( $helper-name, $m );
}
}
}
}
}
my sub typecheck-attr-value ( $attr is raw, $value ) is raw is hidden-from-backtrace {
my $rc;
given $attr.name.substr(0,1) { # Take sigil from attribute name
when '$' {
# Do it via nqp because I didn't find any syntax-based way to properly clone a Scalar container
# as such.
my $v := nqp::create(Scalar);
nqp::bindattr($v, Scalar, '$!descriptor',
nqp::getattr(nqp::decont($attr), Attribute, '$!container_descriptor')
);
# note "SCALAR OF ", $v.VAR.of;
$rc := $v = $value;
}
when '@' {
#note "ASSIGN TO POSITIONAL";
my @a := $attr.auto_viv_container.clone;
#note $value.perl;
$rc := @a = |$value;
}
when '%' {
my %h := $attr.auto_viv_container.clone;
$rc := %h = $value;
}
when '&' {
my &m := nqp::clone($attr.auto_viv_container.VAR);
$rc := &m = $value;
}
default {
die "AttrX::Mooish can't handle «$_» sigil";
}
}
# note "=== RC: ", $rc.VAR.^name, " // ", $rc.VAR.of;
$rc
}
role AttrXMooishAttributeHOW {
has $.base-name = self.name.substr(2);
has $.sigil = self.name.substr( 0, 1 );
has $.always-bind = False;
has $.lazy is rw = False;
has $.builder is rw = 'build-' ~ $!base-name;
has $.clearer is rw = False;
has $.predicate is rw = False;
has $.trigger is rw = False;
has $.filter is rw = False;
has $.composer is rw = False;
has $.no-init is rw = False;
has @.init-args;
my %opt2prefix = clearer => 'clear',
predicate => 'has',
builder => 'build',
trigger => 'trigger',
filter => 'filter',
composer => 'compose',
;
method !bool-str-meth-name( $opt, Str $prefix, Str :$base-name? ) is hidden-from-backtrace {
#note "bool-str-meth-name: ", $prefix;
$opt ~~ Bool ?? $prefix ~ '-' ~ ( $base-name // $!base-name ) !! $opt;
}
method opt2method( Str $oname, Str :$base-name? ) is hidden-from-backtrace {
#note "%opt2prefix: ", %opt2prefix;
#note "option name in opt2method: $oname // ", %opt2prefix{$oname};
self!bool-str-meth-name( self."$oname"(), %opt2prefix{$oname}, :$base-name );
}
method compose ( Mu \type, :$compiler_services ) is hidden-from-backtrace {
# note "+++ composing {$.name} on {type.^name} {type.HOW}";
# note "ATTR PACKAGE:", $.package.^name;
$!always-bind = $!filter || $!trigger;
unless type.HOW ~~ AttrXMooishClassHOW {
#note "Installing AttrXMooishClassHOW on {type.WHICH}";
type.HOW does AttrXMooishClassHOW;
}
for @!init-args -> $alias {
# note "GEN ACCESSOR $alias for {$.name} on {type.^name}";
my $meth := $compiler_services.generate_accessor(
$alias, nqp::decont(type), $.name, nqp::decont( $.type ), $.rw ?? 1 !! 0
);
type.^add_method( $alias, $meth );
}
callsame;
self.invoke-composer( type );
#note "+++ done composing attribute {$.name}";
}
# force-default is true if attribute is set in .new( ) call
method make-mooish ( Mu \instance, %attrinit ) is hidden-from-backtrace {
my $attr = self;
my $obj-id = instance.WHICH;
#note "Using obj ID:", $obj-id;
return if so %attr-data{$obj-id}{$.name};
# note ">>> MOOIFYING ", $.name;
# note ">>> HAS INIT: ", %attrinit;
my $init-key = $.no-init ?? Nil !! ($!base-name, |@!init-args).grep( { %attrinit{$_}:exists } ).head;
# note "=== Taking $!base-name from init? ", ? $init-key;
my $initialized = ? $init-key;
my $default = $initialized ?? %attrinit{$init-key} !! self.get_value( instance );
# note "DEFAULT IS:", $default // $default.WHAT;
unless $initialized { # False means no constructor parameter for the attribute
# note ". No $.name constructor parameter on $obj-id, checking default {$default // '(Nil)'}";
given $default {
when Array | Hash { $initialized = so .elems; }
default { $initialized = .defined }
}
}
%attr-data{$obj-id}{$attr.name}<bound> = False;
self.bind-proxy( instance, $obj-id );
if $initialized {
#note "=== Using initial value ({$initialized} // {$from-init}) ", $default;
my @params;
@params.append( {:constructor} ) if $init-key;
# note "INIT STORE PARAMS: {@params}";
self.store-with-cb( instance, $default, @params );
}
# note "Setting mooished";
#%attr-data{$obj-id}{$.name}<value> = $default;
%attr-data{$obj-id}{$attr.name}<mooished> = True;
# note "<<< DONE MOOIFYING ", $.name;
}
method bind-proxy ( Mu \instance, $obj-id ) is hidden-from-backtrace {
my $attr = self;
return if %attr-data{$obj-id}{$attr.name}<bound>;
# note "++++ BINDING PROXY TO ", $.name;
nqp::bindattr(nqp::decont(instance), $.package, $.name,
Proxy.new(
FETCH => -> $ {
#note "FETCHING";
my $val;
given $!sigil {
when '$' | '&' {
$val = nqp::clone($.auto_viv_container.VAR);
}
default {
$val := $.auto_viv_container.clone;
}
}
# note "IS MOOISHED? ", ? %attr-data{$obj-id}{$attr.name}<mooished>;
if %attr-data{$obj-id}{$attr.name}<mooished> {
# note "FETCH of {$attr.name} for ", $obj-id, ~Backtrace.new.full;
self.build-attr( instance ) if ?$!lazy and %attr-data{$obj-id}{$attr.name}<value>:!exists;
$val := %attr-data{$obj-id}{$attr.name}<value> if %attr-data{$obj-id}{$attr.name}<value>:exists;
# note "Fetched value for {$.name}: ", $val.VAR.^name, " // ", $val.perl;
# Once read and built, mooishing is not needed unless filter or trigger are set; and until
# clearer is called.
self.unbind-proxy( instance, $obj-id, $val );
}
$val
},
STORE => -> $, $value is copy {
self.store-with-cb( instance, $value );
}
)
);
%attr-data{$obj-id}{$.name}<bound> = True;
}
method unbind-proxy ( Mu \instance, $obj-id, $val is raw ) {
unless $!always-bind or !%attr-data{$obj-id}{$.name}<bound> {
# note "---- UNBINDING ATTR {$.name} INTO VALUE ($val // {$val.VAR.^name} // {$val.VAR.of.^name})";
nqp::bindattr( nqp::decont(instance), $.package, $.name, $val );
%attr-data{$obj-id}{$.name}<bound> = False;
}
}
method store-with-cb ( Mu \instance, $value is rw, @params = () ) is hidden-from-backtrace {
#note "INVOKING {$.name} FILTER WITH {@params.perl}";
self.invoke-filter( instance, $value, @params ) if $!filter;
# note "STORING VALUE: ($value)";
self.store-value( instance, instance.WHICH, $value );
#note "INVOKING {$.name} TRIGGER WITH {@params.perl}";
self.invoke-opt( instance, 'trigger', ( $value, |@params ), :strict ) if $!trigger;
}
# store-value would return the value stored.
method store-value ( Mu \instance, $obj-id, $value is copy ) is hidden-from-backtrace {
# note ". storing into {$.name} // ";
# note "store-value for ", $obj-id;
if %attr-data{$obj-id}{$.name}<value>:exists {
given $!sigil {
when '$' | '&' {
nqp::p6assign(%attr-data{$obj-id}{$.name}<value>, $value);
}
when '@' | '%' {
%attr-data{$obj-id}{$.name}<value>.STORE(nqp::decont($value));
}
default {
die "AttrX::Mooish can't handle «$_» sigil";
}
}
}
else {
%attr-data{$obj-id}{$.name}<value> := typecheck-attr-value( self, $value );
}
# note "=== VALUE IN THE HASH: ",
# %attr-data{$obj-id}{$.name}<value>.VAR.^name,
# " // ",
# %attr-data{$obj-id}{$.name}<value>.VAR.of;
self.unbind-proxy(
instance,
$obj-id,
%attr-data{$obj-id}{$.name}<value>
);
}
method is-set ( $obj-id) is hidden-from-backtrace {
#note ". IS-SET( $obj-id ) on {$.name}: ", %attr-data{$obj-id}{$.name};
%attr-data{$obj-id}{$.name}<value>:exists;
}
method clear-attr ( $obj-id ) is hidden-from-backtrace {
# note "Clearing {$.name} on $obj-id";
%attr-data{$obj-id}{$.name}<value>:delete;
}
method invoke-filter ( Mu \instance, $value is rw, @params = () ) is hidden-from-backtrace {
if $!filter {
my $obj-id = instance.WHICH;
my @invoke-params = $value, |@params;
@invoke-params.push( 'old-value' => %attr-data{$obj-id}{$.name}<value> ) if self.is-set( $obj-id );
$value = self.invoke-opt( instance, 'filter', @invoke-params, :strict );
}
}
method invoke-opt (
Any \instance, Str $option, @params = (), :$strict = False, PvtMode :$private is copy = pvmAuto
) is hidden-from-backtrace {
my $opt-value = self."$option"();
my \type = $.package;
return unless so $opt-value;
# note "&&& INVOKING {$option} on {$.name}";
my @invoke-params = :attribute($.name), |@params;
my $method;
sub get-method( $name, Bool $public ) {
$public ??
instance.^find_method( $name, :no_fallback(1) )
!!
type.^find_private_method( $name )
}
given $opt-value {
when Str | Bool {
if $opt-value ~~ Bool {
die "Bug encountered: boolean option $option doesn't have a prefix assigned"
unless %opt2prefix{$option};
$opt-value = "{%opt2prefix{$option}}-{$!base-name}";
# Bool-defined option must always have same privacy as attribute
$private = pvmAsAttr if $private == pvmAuto;
}
my $is-pub = $.has_accessor;
given $private {
when pvmForce | pvmNever {
$method = get-method( $opt-value, $is-pub = $_ == pvmNever );
}
when pvmAsAttr {
$method = get-method( $opt-value, $.has_accessor );
}
when pvmAuto {
$method = get-method( $opt-value, $.has_accessor ) // get-method( $opt-value, !$.has_accessor );
}
}
#note "&&& ON INVOKING: found method ", $method.defined ;
unless so $method {
# If no method found by name die if strict is on
#note "No method found for $option";
return unless $strict;
X::Method::NotFound.new(
method => $opt-value,
private =>!$is-pub,
typename => instance.WHO,
).throw;
}
}
when Callable {
$method = $opt-value;
}
default {
die "Bug encountered: $option is of unsupported type {$opt-value.WHO}";
}
}
#note "INVOKING {$method ~~ Code ?? $method.name !! $method} with ", @invoke-params.Capture;
instance.$method(|(@invoke-params.Capture));
}
method build-attr ( Any \instance ) is hidden-from-backtrace {
my $obj-id = instance.WHICH;
my $publicity = $.has_accessor ?? "public" !! "private";
# note "&&& KINDA BUILDING FOR $publicity {$.name} on $obj-id (is-set:{self.is-set($obj-id)})";
unless self.is-set( $obj-id ) {
#note "&&& Calling builder {$!builder}";
my $val = self.invoke-opt( instance, 'builder', :strict );
self.store-with-cb( instance, $val, [ :builder ] );
#note "Set ATTR";
}
}
method invoke-composer ( Mu \type ) is hidden-from-backtrace {
return unless $!composer;
#note "My type for composer: ", $.package;
my $comp-name = self.opt2method( 'composer' );
#note "Looking for method $comp-name";
my $composer = type.^find_private_method( $comp-name );
X::Method::NotFound.new(
method => $comp-name,
private => True,
typename => type.WHO,
).throw unless $composer;
type.&$composer();
}
}
role AttrXMooishClassHOW does AttrXMooishHelper {
has %!init-arg-cache;
method compose ( Mu \type, :$compiler_services ) is hidden-from-backtrace {
for type.^attributes.grep( AttrXMooishAttributeHOW ) -> $attr {
self.setup-helpers( type, $attr );
}
# note "+++ done composing {type.^name}";
nextsame;
}
method on_DESTROY ($object) {
%attr-data{self.WHICH}:delete;
}
method add_method(Mu \type, $name, $code_obj, :$nowrap=False) is hidden-from-backtrace {
# note "^^^ ADDING METHOD $name on {$obj.^name} defined:{?$obj.?defined} // $nowrap";
my $m = $code_obj;
unless $nowrap {
given $name {
when <DESTROY> {
#note "^^^ WRAPPING DESTROY";
$m = my submethod DESTROY {
# note "&&& REPLACED DESTROY on {self.WHICH} // {self.HOW.^name}";
self.HOW.on_DESTROY( self );
self.&$code_obj;
}
}
}
}
#note "^^^ Done adding method $name";
nextwith(type, $name, $m);
}
method install-stagers ( Mu \type ) is hidden-from-backtrace {
# note "+++ INSTALLING STAGERS {type.WHO} {type.HOW}";
my %wrap-methods;
%wrap-methods<DESTROY> = my submethod {
# note "&&& INSTALLED DESTROY on {self.WHICH} // {self.HOW.^name}";
self.HOW.on_DESTROY( self );
nextsame;
};
my $has-build = type.^declares_method( 'BUILD' );
my $iarg-cache := %!init-arg-cache;
%wrap-methods<BUILD> = my submethod (*%attrinit) {
# note "&&& CUSTOM BUILD on {self.WHO} by {type.WHO} // has-build:{$has-build}";
# Don't pass initial attributes if wrapping user's BUILD - i.e. we don't initialize from constructor
type.^on_create( self, $has-build ?? {} !! %attrinit );
when !$has-build {
# We would have to init all non-mooished attributes from attrinit.
my $base-name;
# note "ATTRINIT: ", %attrinit;
for type.^attributes( :local(1) ).grep( {
$_ !~~ AttrXMooishAttributeHOW
&& .has_accessor
&& (%attrinit{$base-name = .name.substr(2)}:exists)
} ) -> $lattr {
# note "--- INIT PUB ATTR $base-name // ", $lattr.^name;
#note "WHO:", $lattr.WHO;
# my $val = %attrinit{$base-name};
$lattr.set_value( self, typecheck-attr-value( $lattr, %attrinit{$base-name} ) );
}
}
nextsame;
}
for %wrap-methods.keys -> $method-name {
my $orig-method = type.^declares_method( $method-name );
my $my-method = %wrap-methods{$method-name};
$my-method.set_name( $method-name );
if $orig-method {
# note "&&& WRAPPING $method-name";
type.^find_method($method-name, :no_fallback(1)).wrap( $my-method );
}
else {
# note "&&& ADDING $method-name on {type.^name}";
self.add_method( type, $method-name, $my-method );
}
}
type.^setup_finalization;
#type.^compose_repr;
#note "+++ done installing stagers";
}
method create_BUILDPLAN ( Mu \type ) is hidden-from-backtrace {
#note "+++ PREPARE {type.WHO}";
self.install-stagers( type );
callsame;
#note "+++ done create_BUILDPLAN";
}
method on_create ( Mu \type, Mu \instance, %attrinit ) is hidden-from-backtrace {
#note "ON CREATE";
my @lazyAttrs = type.^attributes( :local(1) ).grep( AttrXMooishAttributeHOW );
for @lazyAttrs -> $attr {
# note "Found lazy attr {$attr.name} // {$attr.HOW} // ", $attr.init-args, " --> ", $attr.init-args.elems;
%!init-arg-cache{ $attr.name } = $attr if $attr.init-args.elems > 0;
$attr.make-mooish( instance, %attrinit );
}
}
method slots-used {
#note Dump( $(%attr-data) );
%attr-data.keys.elems;
}
}
role AttrXMooishRoleHOW does AttrXMooishHelper {
method compose (Mu \type, :$compiler_services ) is hidden-from-backtrace {
# note "COMPOSING ROLE ", type.^name, " // ", type.HOW.^name, " // ", ? $compiler_services;
for type.^attributes.grep( AttrXMooishAttributeHOW ) -> $attr {
self.setup-helpers( type, $attr );
}
# note "+++ done composing {type.^name}";
nextsame
}
method specialize(Mu \r, Mu:U \obj, *@pos_args, *%named_args) is hidden-from-backtrace {
#note "*** Specializing role {r.^name} on {obj.WHO}";
#note "CLASS HAS THE ROLE:", obj.HOW ~~ AttrXMooishClassHOW;
obj.HOW does AttrXMooishClassHOW unless obj.HOW ~~ AttrXMooishClassHOW;
#note "*** Done specializing";
nextsame;
}
}
multi trait_mod:<is>( Attribute:D $attr, :$mooish! ) is export {
$attr does AttrXMooishAttributeHOW;