line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package MoobX; |
2
|
|
|
|
|
|
|
our $AUTHORITY = 'cpan:YANICK'; |
3
|
|
|
|
|
|
|
# ABSTRACT: Reactive programming framework heavily inspired by JavaScript's MobX |
4
|
|
|
|
|
|
|
$MoobX::VERSION = '0.1.0'; |
5
|
|
|
|
|
|
|
|
6
|
8
|
|
|
8
|
|
421965
|
use 5.20.0; |
|
8
|
|
|
|
|
23
|
|
7
|
|
|
|
|
|
|
|
8
|
8
|
|
|
8
|
|
2769
|
use MoobX::Observer; |
|
8
|
|
|
|
|
17
|
|
|
8
|
|
|
|
|
382
|
|
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
our @DEPENDENCIES; |
11
|
|
|
|
|
|
|
our $WATCHING = 0; |
12
|
|
|
|
|
|
|
|
13
|
8
|
|
|
8
|
|
43
|
use Scalar::Util qw/ reftype refaddr /; |
|
8
|
|
|
|
|
7
|
|
|
8
|
|
|
|
|
424
|
|
14
|
8
|
|
|
8
|
|
27
|
use Moose::Util qw/ with_traits /; |
|
8
|
|
|
|
|
12
|
|
|
8
|
|
|
|
|
54
|
|
15
|
8
|
|
|
8
|
|
1566
|
use Module::Runtime 'use_module'; |
|
8
|
|
|
|
|
10
|
|
|
8
|
|
|
|
|
41
|
|
16
|
8
|
|
|
8
|
|
3883
|
use Graph::Directed; |
|
8
|
|
|
|
|
609478
|
|
|
8
|
|
|
|
|
236
|
|
17
|
|
|
|
|
|
|
|
18
|
8
|
|
|
8
|
|
59
|
use experimental 'signatures'; |
|
8
|
|
|
|
|
10
|
|
|
8
|
|
|
|
|
59
|
|
19
|
|
|
|
|
|
|
|
20
|
8
|
|
|
8
|
|
1086
|
use parent 'Exporter::Tiny'; |
|
8
|
|
|
|
|
10
|
|
|
8
|
|
|
|
|
58
|
|
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
our @EXPORT = qw/ observer observable autorun :attributes :traits /; |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
our $WARN_NO_DEPS = 1; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
sub _exporter_expand_tag { |
27
|
32
|
|
|
32
|
|
1255
|
my( $class, $name, $args, $globals ) = @_; |
28
|
|
|
|
|
|
|
|
29
|
32
|
100
|
|
|
|
108
|
if ( $name eq 'attributes' ) { |
|
|
50
|
|
|
|
|
|
30
|
16
|
|
|
|
|
29
|
my $target = $globals->{into}; |
31
|
|
|
|
|
|
|
|
32
|
8
|
50
|
|
8
|
|
38
|
eval qq{ |
|
8
|
|
|
8
|
|
12
|
|
|
8
|
|
|
|
|
35
|
|
|
8
|
|
|
|
|
76
|
|
|
8
|
|
|
|
|
12
|
|
|
8
|
|
|
|
|
49
|
|
|
16
|
|
|
|
|
1194
|
|
33
|
|
|
|
|
|
|
package $target; |
34
|
|
|
|
|
|
|
use parent 'MoobX::Attributes'; |
35
|
|
|
|
|
|
|
1; |
36
|
|
|
|
|
|
|
} or die $@; |
37
|
|
|
|
|
|
|
} |
38
|
|
|
|
|
|
|
elsif( $name eq 'traits' ) { |
39
|
16
|
|
|
|
|
92
|
use_module( 'MoobX::Trait::'.$_) for qw/ Observer Observable /; |
40
|
|
|
|
|
|
|
} |
41
|
|
|
|
|
|
|
|
42
|
32
|
|
|
|
|
471
|
return (); |
43
|
|
|
|
|
|
|
} |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
our $graph = Graph::Directed->new; |
46
|
|
|
|
|
|
|
|
47
|
108
|
50
|
|
108
|
0
|
227
|
sub observable_modified($obs) { |
|
108
|
50
|
|
|
|
179
|
|
|
108
|
|
|
|
|
104
|
|
|
108
|
|
|
|
|
141
|
|
48
|
|
|
|
|
|
|
|
49
|
108
|
|
|
|
|
443
|
my @preds = $graph->all_predecessors( refaddr $obs ); |
50
|
|
|
|
|
|
|
|
51
|
108
|
|
|
|
|
11370
|
for my $pred ( @preds ) { |
52
|
32
|
|
|
|
|
135
|
my $info = $graph->get_vertex_attribute( |
53
|
|
|
|
|
|
|
$pred, 'info' |
54
|
|
|
|
|
|
|
); |
55
|
|
|
|
|
|
|
|
56
|
32
|
|
|
|
|
1691
|
local @MoobX::DEPENDENCIES = ( @MoobX::DEPENDENCIES, $obs ); |
57
|
32
|
|
|
|
|
90
|
$info->clear_value; |
58
|
|
|
|
|
|
|
} |
59
|
|
|
|
|
|
|
} |
60
|
|
|
|
|
|
|
|
61
|
36
|
50
|
|
36
|
0
|
79
|
sub dependencies_for($self,@deps) { |
|
36
|
|
|
|
|
39
|
|
|
36
|
|
|
|
|
115
|
|
|
36
|
|
|
|
|
42
|
|
62
|
|
|
|
|
|
|
$graph->delete_edges( |
63
|
|
|
|
|
|
|
map { |
64
|
36
|
|
|
|
|
209
|
refaddr $self => $_ |
|
51
|
|
|
|
|
1330
|
|
65
|
|
|
|
|
|
|
} $graph->successors(refaddr $self) |
66
|
|
|
|
|
|
|
); |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
$graph->add_edges( |
69
|
36
|
|
|
|
|
6686
|
map { refaddr $self => refaddr $_ } @deps |
|
167
|
|
|
|
|
369
|
|
70
|
|
|
|
|
|
|
); |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
$graph->set_vertex_attribute( |
73
|
|
|
|
|
|
|
refaddr $_, info => $_ |
74
|
36
|
|
|
|
|
14764
|
) for $self, @deps; |
75
|
|
|
|
|
|
|
} |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
sub observable :prototype(\[$%@]) { |
78
|
11
|
|
|
11
|
1
|
302
|
observable_ref( @_ ); |
79
|
|
|
|
|
|
|
} |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
sub observable_ref { |
82
|
31
|
|
|
31
|
0
|
61
|
my $ref = shift; |
83
|
|
|
|
|
|
|
|
84
|
31
|
|
|
|
|
100
|
my $type = reftype $ref; |
85
|
|
|
|
|
|
|
|
86
|
31
|
|
50
|
|
|
160
|
my $class = 'MoobX::'. ucfirst lc $type || 'SCALAR'; |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
$class = with_traits( |
89
|
62
|
|
|
|
|
584
|
map { use_module($_) } |
90
|
31
|
|
|
|
|
55
|
map { $_, $_ . '::Observable' } $class |
|
31
|
|
|
|
|
97
|
|
91
|
|
|
|
|
|
|
); |
92
|
|
|
|
|
|
|
|
93
|
31
|
100
|
|
|
|
55195
|
if( $type eq 'SCALAR' ) { |
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
94
|
9
|
|
|
|
|
16
|
my $value = $$ref; |
95
|
9
|
|
|
|
|
56
|
tie $$ref, $class; |
96
|
9
|
|
|
|
|
2922
|
$$ref = $value; |
97
|
|
|
|
|
|
|
} |
98
|
|
|
|
|
|
|
elsif( $type eq 'ARRAY' ) { |
99
|
17
|
|
|
|
|
40
|
my @values = @$ref; |
100
|
17
|
|
|
|
|
82
|
tie @$ref, $class; |
101
|
17
|
|
|
|
|
7736
|
@$ref = @values; |
102
|
|
|
|
|
|
|
} |
103
|
|
|
|
|
|
|
elsif( $type eq 'HASH' ) { |
104
|
5
|
|
|
|
|
18
|
my %values = %$ref; |
105
|
5
|
|
|
|
|
35
|
tie %$ref, $class; |
106
|
5
|
|
|
|
|
2495
|
%$ref = %values; |
107
|
|
|
|
|
|
|
} |
108
|
|
|
|
|
|
|
elsif( not $type ) { |
109
|
0
|
|
|
|
|
0
|
my $value = $ref; |
110
|
0
|
|
|
|
|
0
|
tie $ref, $class; |
111
|
0
|
|
|
|
|
0
|
$ref = $value; |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
|
115
|
31
|
|
|
|
|
221
|
return $ref; |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
} |
118
|
|
|
|
|
|
|
|
119
|
6
|
|
|
6
|
1
|
531
|
sub observer :prototype(&) { MoobX::Observer->new( generator => @_ ) } |
120
|
4
|
|
|
4
|
1
|
73
|
sub autorun :prototype(&) { MoobX::Observer->new( autorun => 1, generator => @_ ) } |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
1; |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
__END__ |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
=pod |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
=encoding UTF-8 |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
=head1 NAME |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
MoobX - Reactive programming framework heavily inspired by JavaScript's MobX |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
=head1 VERSION |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
version 0.1.0 |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
=head1 SYNOPSIS |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
use 5.20.0; |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
use MoobX; |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
my $first_name :Observable; |
145
|
|
|
|
|
|
|
my $last_name :Observable; |
146
|
|
|
|
|
|
|
my $title :Observable; |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
my $address = observer { |
149
|
|
|
|
|
|
|
join ' ', $title || $first_name, $last_name; |
150
|
|
|
|
|
|
|
}; |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
say $address; # nothing |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
$first_name = "Yanick"; |
155
|
|
|
|
|
|
|
$last_name = "Champoux"; |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
say $address; # Yanick Champoux |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
$title = 'Dread Lord'; |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
say $address; # Dread Lord Champoux |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
=head1 DESCRIPTION |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
As I was learning how to use L<https://github.com/mobxjs/mobx|MobX>, I thought |
166
|
|
|
|
|
|
|
it'd be fun to try to implement something similar in Perl. So I did. |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
To set Moose object attributes to be observers or observables, take |
169
|
|
|
|
|
|
|
a gander at L<MoobX::Trait::Observable> and L<MoobX::Trait::Observer>. |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
To have an idea of the mechanics of MoobX, see the two blog entries in the SEE ALSO |
172
|
|
|
|
|
|
|
section. |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
This is also the early stages of life for this module. Consider everythign as alpha quality, |
175
|
|
|
|
|
|
|
and the API still subject to huge changes. |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
=head1 EXPORTED FUNCTIONS |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
The module automatically exports 3 functions: C<observer>, C<observable> and C<autorun>. |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
=head2 observable |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
observable my $foo; |
184
|
|
|
|
|
|
|
observable my @bar; |
185
|
|
|
|
|
|
|
observable my %quux; |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
Marks the variable as an observable, i.e. a variable which value can be |
188
|
|
|
|
|
|
|
watched by observers, which will be updated when it changes. |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
Under the hood, the variable is tied to the relevant L<MoobX::TYPE> class |
191
|
|
|
|
|
|
|
L<MoobX::TYPE::Observable> role. |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
If you want to declare the variable, assign it a value and set it as observable, |
194
|
|
|
|
|
|
|
there are a few good ways to do it, and one bad: |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
my $foo = 3; |
197
|
|
|
|
|
|
|
observable $foo; # good |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
observable( my $foo = 3 ); # good |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
observable my $foo; # good |
202
|
|
|
|
|
|
|
$foo = 3; |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
observable my $foo = 3; # bad |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
That last one doesn't work because Perl parses it as C<observable( my $foo ) = 3>, |
207
|
|
|
|
|
|
|
and assigning values to non I<lvalue>ed functions don't work. |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
Or, better, simply use the C<:Observable> attribute when you define the variable. |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
my $foo :Observable = 2; |
212
|
|
|
|
|
|
|
my @bar :Observable = 1..10; |
213
|
|
|
|
|
|
|
my %baz :Observable = ( a => 1, b => 2 ); |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=head2 observer |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
observable my $quantity; |
218
|
|
|
|
|
|
|
observable my $price; |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
my $total = observer { |
221
|
|
|
|
|
|
|
$quantity * $price |
222
|
|
|
|
|
|
|
}; |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
$quantity = 2; |
225
|
|
|
|
|
|
|
$price = 6.00; |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
print $total; # 12 |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
Creates a L<MoobX::Observer> object. The value returned by the object will |
230
|
|
|
|
|
|
|
react to change to any C<observable> values within its definition. |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
Observers are lazy, meaning that they compute or recompute their values |
233
|
|
|
|
|
|
|
when they are accessed. If you want |
234
|
|
|
|
|
|
|
them to eagerly recompute their values, C<autorun> is what you want. |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
If an observer function is run and doesn't report any dependency, |
237
|
|
|
|
|
|
|
it'll emit the warning 'C<MoobX observer doesn't observe anything>', |
238
|
|
|
|
|
|
|
because chances are there's something weird going on. The warning can |
239
|
|
|
|
|
|
|
be silenced via the global variable C<$MoobX::WARN_NO_DEPS>. |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
my $foo :Observable; |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
my $debugging = 0; |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
# if $debugging == 1, we'd get a warning |
246
|
|
|
|
|
|
|
local $MoobX::WARN_NO_DEPS = 0; |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
my $spy = observer { |
249
|
|
|
|
|
|
|
return unless $debugging; |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
say $foo; |
252
|
|
|
|
|
|
|
}; |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
=head2 autorun |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
observable my $foo; |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
autorun { |
259
|
|
|
|
|
|
|
say "\$foo is now $foo"; |
260
|
|
|
|
|
|
|
}; |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
$foo = 1; # prints '$foo is now 1' |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
$foo = 2; # prints '$foo is now 2' |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
Like C<observer>, but immediatly recompute its value when its observable dependencies change. |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
=head1 SEE ALSO |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
=over |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
=item L<https://github.com/mobxjs/mobx|MobX> - the original inspiration |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
=item L<http://techblog.babyl.ca/entry/moobx> and L<http://techblog.babyl.ca/entry/moobx-2> - the two blog entries that introduced MobX. |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
=back |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
=head1 AUTHOR |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
Yanick Champoux <yanick@cpan.org> |
281
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
This software is copyright (c) 2017 by Yanick Champoux. |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
287
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
=cut |