File Coverage

blib/lib/Types/Algebraic.pm
Criterion Covered Total %
statement 212 254 83.4
branch 24 34 70.5
condition 9 23 39.1
subroutine 43 47 91.4
pod n/a
total 288 358 80.4


line stmt bran cond sub pod time code
1             package Types::Algebraic;
2              
3 11     11   784230 use strict;
  11         117  
  11         358  
4 11     11   389 use 5.022;
  11         46  
5 11     11   56 use warnings;
  11         20  
  11         666  
6             our $VERSION = '0.07';
7              
8 11     11   74 use Carp qw(croak confess);
  11         34  
  11         630  
9 11     11   7452 use Data::Dumper;
  11         79249  
  11         897  
10 11     11   97 use List::Util qw(all);
  11         25  
  11         1212  
11 11     11   7984 use List::MoreUtils qw(pairwise zip_unflatten);
  11         159840  
  11         82  
12 11     11   25200 use Keyword::Declare;
  11         1293520  
  11         132  
13 11     11   3061 use Keyword::Simple;
  11         28  
  11         301  
14 11     11   8500 use Moops;
  11         198647  
  11         77  
15 11     11   719658 use PPR;
  11         32  
  11         515  
16 11     11   77 use Scalar::Util qw(blessed);
  11         26  
  11         3247  
17              
18             our $_RETURN_SENTINEL = \23;
19              
20             our %_KNOWN_CONSTRUCTORS;
21              
22             my ($expected, $fail_loc) = ('match statement', 0);
23             our $_TA_REGEX_LIB = qr{
24             (?(DEFINE)
25             (?<ADTPattern>
26             \( (?&PerlOWS)
27             (?{ $expected = "the name of a constructor", $fail_loc = pos() })
28             (?&PerlIdentifier) # constructor
29             (?{ $expected = "zero or more constructor arguments", $fail_loc = pos() })
30             (?:(?&PerlNWS) (?&ADTPatternSegment))* # 0 or more arguments
31             (?&PerlOWS)
32             \)
33             )
34              
35             (?<ADTPatternSegment>
36             (?:
37             \$ (?&PerlIdentifier) | # variable
38             (?&PerlIdentifier) | # constuctor without arguments
39             (?&ADTPattern) # constructor with arguments - requires parentheses
40             )
41             )
42             )
43              
44             $PPR::GRAMMAR
45             }xms;
46              
47 11     11   1477231 class ADT {
  11     1   426  
  11     1   113  
  11     1   32  
  11     1   817  
  11     1   7684  
  11     1   26687  
  11     1   52  
  11     1   22016  
  11     1   32  
  11     1   93  
  11     1   1824  
  11     1   28  
  11     11   668  
  11         74  
  11         28  
  11         1346  
  11         425  
  11         130  
  11         26  
  11         140  
  11         59174  
  11         32  
  11         103  
  11         13079  
  11         42369  
  11         60  
  11         438103  
  11         35  
  11         246  
  11         8620  
  11         107353  
  11         134  
  11         10351  
  11         34181  
  11         103  
  11         19496  
  11         35813  
  11         106  
  11         1696223  
  11         43  
  11         69  
  11         28  
  11         291  
  11         61  
  11         24  
  11         494  
  11         63  
  11         24  
  11         9943  
  11         52116  
  0         0  
48 11         209 has tag => (is => "ro", isa => Str);
49 11         29294 has values => (is => "ro", isa => ArrayRef);
50              
51             sub _equality {
52 74     74   161 my ($type, $x, $y) = @_;
53              
54 74 100 50     487 return 0 unless ref($x) && (ref($x) // '') eq (ref($y) // '');
      50        
      66        
55 29 100       178 return 0 unless $x->tag eq $y->tag;
56 24 50   22   103 return List::Util::all { $_ } List::MoreUtils::pairwise { $type eq '==' ? $a == $b : $a eq $b } @{$x->values}, @{$y->values};
  22         130  
  22         77  
  24         59  
  24         310  
57             }
58              
59 0     0   0 sub _equality_num { return _equality('==', @_); }
60 0     0   0 sub _equality_str { return _equality('eq', @_); }
61              
62             sub _stringify {
63 1044     1044   3393 my $v = shift;
64 1044         1851 return $v->tag . "(" . join(", ", map { "$_" } @{ $v->values }) . ")";
  1582         4470  
  1044         2853  
65             }
66              
67             use overload
68 0     0   0 '==' => sub { _equality('==', @_) },
  0         0  
69 0     45   0 '!=' => sub { ! _equality('==', @_) },
  45         1916  
70 0     22   0 'eq' => sub { _equality('eq', @_) },
  22         10514  
71 0     7   0 'ne' => sub { ! _equality('eq', @_) },
  7         11218  
72 1     1   8 '""' => \&_stringify;
  1     10   27  
  1         15  
  10         90  
  10         24  
  10         148  
73             }
74              
75             sub _apply_pattern {
76 438     438   63827 my ($value, $pattern) = @_;
77              
78 438 100       890 if ($pattern->{type} eq 'variable') {
79 173         435 return (1, [$value]);
80             }
81              
82 265 50 33     650 return 0 unless $value && blessed($value) && $value->isa('Types::Algebraic::ADT');
      33        
83              
84 265 100       1023 return 0 unless $pattern->{constructor} eq $value->tag;
85              
86 167         240 my @variables;
87 167         218 for my $pair (List::MoreUtils::zip_unflatten(@{$value->values}, @{$pattern->{arguments}})) {
  167         305  
  167         765  
88 259         515 my ($rv, $new_vars) = _apply_pattern(@$pair);
89 259 100       579 return 0 unless $rv;
90 215         450 push(@variables, @$new_vars);
91             }
92              
93 123         344 return (1, \@variables);
94             }
95              
96 11     11   347462 keytype ADTConstructor is / (?<tag> (?&PerlIdentifier)) (?<fields> (?: (?&PerlNWS) : (?&PerlIdentifier) )* ) /x;
97              
98             sub _verify_constructor {
99 39     39   242 my ($constructor, $arity) = @_;
100 39         209 my $info = $_KNOWN_CONSTRUCTORS{$constructor};
101              
102 39 50       200 confess("Unknown constructor '$constructor'. Pattern matching requires types created through Types::Algebraic.")
103             unless $info;
104              
105             confess("Constructor '$constructor' expects $info->{arg_count} arguments - but is pattern matched with $arity.")
106 39 50       266 unless $info->{arg_count} == $arity;
107              
108             }
109              
110             sub _parse_pattern {
111 33     33   117 my ($pattern) = @_;
112 33         909303 $pattern =~ m{
113             \A
114             \(
115             (?&PerlOWS)
116             (?<tag>(?&PerlIdentifier))
117             (?<identifiers> (?:(?&PerlNWS) (?&ADTPatternSegment))* )
118             (?&PerlOWS)
119             \)
120             $Types::Algebraic::_TA_REGEX_LIB
121             }xms;
122 33         6073 my ($tag, $idents) = @+{qw(tag identifiers)};
123              
124 33         165 my @segments;
125             my @variables;
126 33         903095 while ($idents =~ m/(?&PerlNWS) (?<segment>(?&ADTPatternSegment)) $Types::Algebraic::_TA_REGEX_LIB/xmsg) {
127 29         4599 my $segment = $+{segment};
128              
129 29 100       280 if ($segment =~ m/^\$/) {
    100          
130 21         174 push(@segments, { type => 'variable', value => $segment });
131 21         565068 push(@variables, $segment);
132             } elsif ($segment =~ m/^\(/) {
133 2         21 my ($parsed, $new_vars) = _parse_pattern($segment);
134 2         14 push(@segments, $parsed);
135 2         53721 push(@variables, @$new_vars);
136             } else {
137 6         43 _verify_constructor($segment, 0);
138              
139 6         163946 push(@segments, { type => 'pattern', constructor => $segment, arguments => [] });
140             }
141             }
142              
143 33         5704 _verify_constructor($tag, scalar @segments);
144              
145 33         2034 return ({ type => 'pattern', constructor => $tag, arguments => \@segments }, \@variables);
146             }
147              
148 0         0 sub import {
149 11     11   282 Moops->import;
150              
151             Keyword::Simple::define 'match', sub {
152 13     13   151831 my ($doc_src) = @_;
153              
154 13         63 ($expected, $fail_loc) = ('match statement', 0);
155 13         39 my $curly_open = '{';
156              
157 13 50       354325 $$doc_src =~ s{
158             \A
159             (?&PerlNWS)
160 13         2400 (?{ $expected = "parenthesized expression", $fail_loc = pos() })
161             (?<matched_expression> (?&PerlParenthesesList) )
162             (?&PerlNWS)
163 13         217 (?{ $expected = "a '$curly_open'", $fail_loc = pos() })
164             \{ (?&PerlOWS)
165              
166             $Types::Algebraic::_TA_REGEX_LIB
167             }{}xms or croak("Invalid match statement.\nExpected: $expected\nFound: ", substr($$doc_src, $fail_loc) =~ /(\S+)/,"\n");
168              
169 13         2409 my $expr = $+{matched_expression};
170              
171 13         67 my $res = "{\n";
172 13         56 my $match_body = $expr . "->match(\n";
173              
174 13         133 while ($$doc_src =~ m/^(?:with|default)/) {
175 35 50       957755 $$doc_src =~ s{
176             \A
177 35         909 (?{ $expected = "a with or default statement", $fail_loc = pos() })
178             (?:
179             with (?&PerlNWS)
180 31         371 (?{ $expected = "a match pattern", $fail_loc = pos() })
181             (?<with_pattern> (?&ADTPattern))
182             |
183             (?<default> default)
184             ) (?&PerlOWS)
185 35         12811 (?{ $expected = "a code block", $fail_loc = pos() })
186             (?<block> (?&PerlBlock) ) (?&PerlOWS)
187              
188             $Types::Algebraic::_TA_REGEX_LIB
189             }{}xms or croak("Invalid match statement.\nExpected: $expected\nFound: ", substr($$doc_src, $fail_loc) =~ /(\S+)/,"\n");
190              
191 35         13810 my ($default, $pattern, $block) = @+{qw(default with_pattern block)};
192              
193 35 100       254 if ($default) {
194 4         204 $match_body .= "[ sub { $block; return \$Types::Algebraic::_RETURN_SENTINEL; } ],\n";
195             } else {
196 31         190 my ($parsed, $variables) = _parse_pattern($pattern);
197              
198 31         217 local $Data::Dumper::Indent = 0;
199 31         102 local $Data::Dumper::Terse = 1;
200              
201 31         311 my $flattened_pattern = Data::Dumper::Dumper($parsed);
202 31         4893 my $args = join(',', @$variables);
203              
204 31         851 $match_body .= "[$flattened_pattern, sub { my ($args) = \@_; $block; return \$Types::Algebraic::_RETURN_SENTINEL; } ],\n";
205             }
206             }
207 13         53 $match_body .= ")";
208              
209 13         46 my $curly_close = '}';
210 13 50       358603 $$doc_src =~ s{
211             \A
212 13         412 (?{ $expected = "a '$curly_close'", $fail_loc = pos() })
213             \}
214              
215             $Types::Algebraic::_TA_REGEX_LIB
216             }{}xms or croak("Invalid match statement.\nExpected: $expected\nFound: ", substr($$doc_src, $fail_loc) =~ /(\S+)/,"\n");
217              
218 13         2158 $res .= <<"EOF";
219             if (wantarray) {
220             my \@types_algebraic_match_result = $match_body;
221             if (\@types_algebraic_match_result != 1 || \$types_algebraic_match_result[0] != \$Types::Algebraic::_RETURN_SENTINEL) { return \@types_algebraic_match_result };
222             } else {
223             my \$types_algebraic_match_result = $match_body;
224             if (\$types_algebraic_match_result && \$types_algebraic_match_result != \$Types::Algebraic::_RETURN_SENTINEL) { return \$types_algebraic_match_result; }
225             }
226             EOF
227 13         102 $res .= "}\n";
228             #say STDERR "\n\n\n$res\n\n-----\n";
229 13         31187 $$doc_src = $res . $$doc_src;
230 11         14269 };
231 11         196  
232 0 50 50 12   0 keyword data (Ident $name, '=', ADTConstructor* @constructors :sep(/\|/)) {
  0         0  
  0         0  
  11         232  
  12         713363  
  12         36  
  12         42  
233 0         0 my %ARGS;
  11         56  
  12         43  
234 0         0 for my $constructor (@constructors) {
  12         321  
235             my $tag = $constructor->{tag};
236 0         0  
  0         0  
  0         0  
  12         500  
  12         94  
  12         30  
237 0         0 my @args;
  12         34  
238 0   0     0 while ($constructor->{fields} =~ m/ (?&PerlNWS) : (?<ident> (?&PerlIdentifier) ) $PPR::GRAMMAR/xg ) {
  12   66     328727  
239 0         0 push(@args, $+{ident});
  29         463464  
240             }
241              
242 0         0 $ARGS{$tag} = scalar @args;
  12         324679  
243              
244 0         0 $_KNOWN_CONSTRUCTORS{$tag} = {
  12         55  
245 0         0 typename => $name,
  12         33  
246 0         0 arg_count => scalar @args,
  12         54  
247 0         0 };
  29         531  
248             }
249 0         0  
  29         87  
250 0         0 my $args_str = join(", ", map { "$_ => $ARGS{$_}" } keys %ARGS);
  29         786803  
251 0         0  
  14         382146  
252             my $res = <<CODE;
253             class $name extends Types::Algebraic::ADT {
254 0         0 my %ARGS = ( $args_str );
  29         4029  
255             CODE
256 0         0  
  29         279  
257             $res .= <<'CODE';
258             sub BUILD {
259             my ($self, $args) = @_;
260             my $tag = $args->{tag} || confess("tag is required - please use public interface");
261             my $values = $args->{values} || confess("values is required - please use public interface");
262 0         0  
  0         0  
  12         139  
  29         253  
263             confess("Unknown constructor $tag") unless exists $ARGS{$tag};
264 0         0 confess("$tag expects $ARGS{$tag} arguments - given ".scalar @$values) unless @$values == $ARGS{$tag};
  12         230  
265             }
266              
267             sub match {
268             my $self = shift;
269 0         0 for my $case (@_) {
  12         205  
270             if (@$case == 2) {
271             my ($pattern, $f) = @$case;
272              
273             my ($matches, $values) = Types::Algebraic::_apply_pattern($self, $pattern);
274             return $f->(@$values) if $matches;
275             }
276             # default
277             if (@$case == 1) {
278             return $case->[0]->(@{ $self->values });
279             }
280             }
281             }
282             }
283             CODE
284              
285             for my $key (keys %ARGS) {
286             $res .= <<CODE;
287             sub $key { return $name->new( tag => '$key', values => [\@_] ); }
288             CODE
289              
290             }
291              
292             #say STDERR $res;
293              
294             return $res;
295             }
296              
297 0         0 }
  12         62  
298 0         0  
  29         534  
299 0     0   0 sub unimport {
300 0 0 0     0 unkeyword data;
  0         0  
301 0         0 Keyword::Simple::undefine 'match';
  0         0  
302 11     11   308171 }
303 11         2905  
304             1;
305             __END__
306 0         0  
  12         434  
307 11         113 =encoding utf-8
  11         117  
308 11         636  
309 11     11   453234 =head1 NAME
310              
311             Types::Algebraic - Algebraic data types in perl
312              
313             =head1 SYNOPSIS
314              
315             use Types::Algebraic;
316              
317             data Maybe = Nothing | Just :v;
318              
319             my $sum = 0;
320             my @vs = ( Nothing, Just(5), Just(7), Nothing, Just(6) );
321             for my $v (@vs) {
322             match ($v) {
323             with (Nothing) { }
324             with (Just $v) { $sum += $v; }
325             }
326             }
327             say $sum;
328              
329             =head1 DESCRIPTION
330              
331             Types::Algebraic is an implementation of L<algebraic data types|https://en.wikipedia.org/wiki/Algebraic_data_type> in perl.
332              
333             These kinds of data types are often seen in functional languages, and allow you to create and consume structured data containers very succinctly.
334              
335             The module provides two keywords: L</"data"> for creating a new data type, and L</"match"> to provide pattern matching on the type.
336              
337             =head1 USAGE
338              
339             =head2 Creating a new type with C<data>
340              
341             The C<data> keyword is used for creating a new type.
342              
343             The code
344              
345             data Maybe = Nothing | Just :v;
346              
347             creates a new type, of name C<Maybe>, which has 2 I<data constructors>, C<Nothing> (taking no parameters), and C<Just> (taking 1 parameter).
348              
349             You may insantiate values of this type by using one of the constructors with the appropriate number of arguments.
350              
351             my $a = Nothing;
352             my $b = Just 5;
353              
354             =head2 Unpacking values with C<match>
355              
356             In order to access the data stored within one of these values, you can use the C<match> keyword.
357              
358             my $value = Just 7;
359             match ($value) {
360             with (Nothing) { say "There was nothing in there. :("; }
361             with (Just $v) { say "I got the value $v!"; }
362             }
363              
364             The cases are matched from the top down, and only the first matching case is run.
365              
366             You can also create a default fallback case, which will always run if reached.
367              
368             data Color = Red | Blue | Green | White | Black;
369             match ($color) {
370             with (Red) { say "Yay, you picked my favorite color!"; }
371             default { say "Bah. You clearly have no taste."; }
372             }
373              
374             =head2 Nested patterns
375              
376             Note, patterns can be nested, allowing for more complex unpacking:
377              
378             data PairingHeap = Empty | Heap :head :subheaps;
379             data Pair = Pair :left :right;
380              
381             # Merge two pairing heaps (https://en.wikipedia.org/wiki/Pairing_heap)
382             sub merge {
383             my ($h1, $h2) = @_;
384              
385             match (Pair($h1, $h2)) {
386             with (Pair Empty $h) { return $h; }
387             with (Pair $h Empty) { return $h; }
388             with (Pair (Heap $e1 $s1) (Heap $e2 $s2)) {
389             return $e1 < $e2 ? Heap($e1, [$h2, @$s1])
390             : Heap($e2, [$h1, @$s2]);
391             }
392             }
393             }
394              
395             =head1 LIMITATIONS
396              
397             =over 4
398              
399             =item Currently, match statements can't be nested.
400              
401             =back
402              
403             =head1 BUGS
404              
405             Please report bugs directly on L<the project's GitHub page|https://github.com/Eckankar/Types-Algebraic>.
406              
407             =head1 AUTHOR
408              
409             Sebastian Paaske Tørholm E<lt>sebbe@cpan.orgE<gt>
410              
411             =head1 COPYRIGHT
412              
413             Copyright 2020- Sebastian Paaske Tørholm
414              
415             =head1 LICENSE
416              
417             This library is free software; you can redistribute it and/or modify
418             it under the same terms as Perl itself.
419              
420             =head1 SEE ALSO
421              
422             =cut