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   804847 use strict;
  11         127  
  11         344  
4 11     11   330 use 5.022;
  11         42  
5 11     11   56 use warnings;
  11         20  
  11         708  
6             our $VERSION = '0.06';
7              
8 11     11   90 use Carp qw(croak confess);
  11         20  
  11         668  
9 11     11   7528 use Data::Dumper;
  11         80338  
  11         880  
10 11     11   100 use List::Util qw(all);
  11         23  
  11         1221  
11 11     11   8320 use List::MoreUtils qw(pairwise zip_unflatten);
  11         164479  
  11         84  
12 11     11   26278 use Keyword::Declare;
  11         1303789  
  11         135  
13 11     11   3133 use Keyword::Simple;
  11         26  
  11         311  
14 11     11   8719 use Moops;
  11         202035  
  11         78  
15 11     11   723737 use PPR;
  11         29  
  11         505  
16 11     11   81 use Scalar::Util qw(blessed);
  11         34  
  11         3277  
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   1487626 class ADT {
  11     1   393  
  11     1   114  
  11     1   23  
  11     1   756  
  11     1   7119  
  11     1   25792  
  11     1   56  
  11     1   22863  
  11     1   28  
  11     1   98  
  11     1   1730  
  11     1   28  
  11     11   726  
  11         76  
  11         25  
  11         1375  
  11         475  
  11         79  
  11         27  
  11         198  
  11         59664  
  11         34  
  11         108  
  11         13720  
  11         43448  
  11         125  
  11         441969  
  11         30  
  11         273  
  11         8898  
  11         106503  
  11         156  
  11         11239  
  11         35885  
  11         96  
  11         20630  
  11         36903  
  11         117  
  11         1751052  
  11         46  
  11         89  
  11         32  
  11         312  
  11         61  
  11         27  
  11         519  
  11         69  
  11         35  
  11         10066  
  11         52549  
  0         0  
48 11         237 has tag => (is => "ro", isa => Str);
49 11         28706 has values => (is => "ro", isa => ArrayRef);
50              
51             sub _equality {
52 74     74   245 my ($type, $x, $y) = @_;
53              
54 74 100 50     531 return 0 unless ref($x) && (ref($x) // '') eq (ref($y) // '');
      50        
      66        
55 29 100       203 return 0 unless $x->tag eq $y->tag;
56 24 50   22   124 return List::Util::all { $_ } List::MoreUtils::pairwise { $type eq '==' ? $a == $b : $a eq $b } @{$x->values}, @{$y->values};
  22         127  
  22         91  
  24         68  
  24         342  
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   3298 my $v = shift;
64 1044         1838 return $v->tag . "(" . join(", ", map { "$_" } @{ $v->values }) . ")";
  1582         4477  
  1044         2890  
65             }
66              
67             use overload
68 0     0   0 '==' => sub { _equality('==', @_) },
  0         0  
69 0     45   0 '!=' => sub { ! _equality('==', @_) },
  45         1890  
70 0     22   0 'eq' => sub { _equality('eq', @_) },
  22         10925  
71 0     7   0 'ne' => sub { ! _equality('eq', @_) },
  7         12023  
72 1     1   10 '""' => \&_stringify;
  1     10   37  
  1         22  
  10         96  
  10         24  
  10         156  
73             }
74              
75             sub _apply_pattern {
76 438     438   62972 my ($value, $pattern) = @_;
77              
78 438 100       897 if ($pattern->{type} eq 'variable') {
79 173         438 return (1, [$value]);
80             }
81              
82 265 50 33     608 return 0 unless $value && blessed($value) && $value->isa('Types::Algebraic::ADT');
      33        
83              
84 265 100       915 return 0 unless $pattern->{constructor} eq $value->tag;
85              
86 167         246 my @variables;
87 167         220 for my $pair (List::MoreUtils::zip_unflatten(@{$value->values}, @{$pattern->{arguments}})) {
  167         319  
  167         674  
88 259         503 my ($rv, $new_vars) = _apply_pattern(@$pair);
89 259 100       559 return 0 unless $rv;
90 215         453 push(@variables, @$new_vars);
91             }
92              
93 123         351 return (1, \@variables);
94             }
95              
96 11     11   352863 keytype ADTConstructor is / (?<tag> (?&PerlIdentifier)) (?<fields> (?: (?&PerlNWS) : (?&PerlIdentifier) )* ) /x;
97              
98             sub _verify_constructor {
99 39     39   213 my ($constructor, $arity) = @_;
100 39         189 my $info = $_KNOWN_CONSTRUCTORS{$constructor};
101              
102 39 50       186 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       214 unless $info->{arg_count} == $arity;
107              
108             }
109              
110             sub _parse_pattern {
111 33     33   100 my ($pattern) = @_;
112 33         891542 $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         4477 my ($tag, $idents) = @+{qw(tag identifiers)};
123              
124 33         154 my @segments;
125             my @variables;
126 33         890857 while ($idents =~ m/(?&PerlNWS) (?<segment>(?&ADTPatternSegment)) $Types::Algebraic::_TA_REGEX_LIB/xmsg) {
127 29         3762 my $segment = $+{segment};
128              
129 29 100       251 if ($segment =~ m/^\$/) {
    100          
130 21         183 push(@segments, { type => 'variable', value => $segment });
131 21         566756 push(@variables, $segment);
132             } elsif ($segment =~ m/^\(/) {
133 2         13 my ($parsed, $new_vars) = _parse_pattern($segment);
134 2         10 push(@segments, $parsed);
135 2         53217 push(@variables, @$new_vars);
136             } else {
137 6         39 _verify_constructor($segment, 0);
138              
139 6         159024 push(@segments, { type => 'pattern', constructor => $segment, arguments => [] });
140             }
141             }
142              
143 33         4378 _verify_constructor($tag, scalar @segments);
144              
145 33         1639 return ({ type => 'pattern', constructor => $tag, arguments => \@segments }, \@variables);
146             }
147              
148 0         0 sub import {
149 11     11   325 Moops->import;
150              
151             Keyword::Simple::define 'match', sub {
152 13     13   150003 my ($doc_src) = @_;
153              
154 13         59 ($expected, $fail_loc) = ('match statement', 0);
155 13         41 my $curly_open = '{';
156              
157 13 50       351920 $$doc_src =~ s{
158             \A
159             (?&PerlNWS)
160 13         2423 (?{ $expected = "parenthesized expression", $fail_loc = pos() })
161             (?<matched_expression> (?&PerlParenthesesList) )
162             (?&PerlNWS)
163 13         193 (?{ $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         2048 my $expr = $+{matched_expression};
170              
171 13         65 my $res = "{\n";
172 13         52 my $match_body = $expr . "->match(\n";
173              
174 13         122 while ($$doc_src =~ m/^(?:with|default)/) {
175 35 50       943918 $$doc_src =~ s{
176             \A
177 35         730 (?{ $expected = "a with or default statement", $fail_loc = pos() })
178             (?:
179             with (?&PerlNWS)
180 31         348 (?{ $expected = "a match pattern", $fail_loc = pos() })
181             (?<with_pattern> (?&ADTPattern))
182             |
183             (?<default> default)
184             ) (?&PerlOWS)
185 35         12810 (?{ $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         11873 my ($default, $pattern, $block) = @+{qw(default with_pattern block)};
192              
193 35 100       217 if ($default) {
194 4         183 $match_body .= "[ sub { $block; return \$Types::Algebraic::_RETURN_SENTINEL; } ],\n";
195             } else {
196 31         157 my ($parsed, $variables) = _parse_pattern($pattern);
197              
198 31         185 local $Data::Dumper::Indent = 0;
199 31         97 local $Data::Dumper::Terse = 1;
200              
201 31         266 my $flattened_pattern = Data::Dumper::Dumper($parsed);
202 31         4520 my $args = join(',', @$variables);
203              
204 31         731 $match_body .= "[$flattened_pattern, sub { my ($args) = \@_; $block; return \$Types::Algebraic::_RETURN_SENTINEL; } ],\n";
205             }
206             }
207 13         47 $match_body .= ")";
208              
209 13         46 my $curly_close = '}';
210 13 50       355464 $$doc_src =~ s{
211             \A
212 13         383 (?{ $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         2121 $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         119 $res .= "}\n";
228             #say STDERR "\n\n\n$res\n\n-----\n";
229 13         31710 $$doc_src = $res . $$doc_src;
230 11         14480 };
231 11         196  
232 0 50 50 12   0 keyword data (Ident $name, '=', ADTConstructor* @constructors :sep(/\|/)) {
  0         0  
  0         0  
  11         220  
  12         709026  
  12         41  
  12         40  
233 0         0 my %ARGS;
  11         91  
  12         39  
234 0         0 for my $constructor (@constructors) {
  12         323  
235             my $tag = $constructor->{tag};
236 0         0  
  0         0  
  0         0  
  12         502  
  12         28  
  12         39  
237 0         0 my @args;
  12         33  
238 0   0     0 while ($constructor->{fields} =~ m/ (?&PerlNWS) : (?<ident> (?&PerlIdentifier) ) $PPR::GRAMMAR/xg ) {
  12   66     327587  
239 0         0 push(@args, $+{ident});
  29         464649  
240             }
241              
242 0         0 $ARGS{$tag} = scalar @args;
  12         322280  
243              
244 0         0 $_KNOWN_CONSTRUCTORS{$tag} = {
  12         51  
245 0         0 typename => $name,
  12         38  
246 0         0 arg_count => scalar @args,
  12         53  
247 0         0 };
  29         546  
248             }
249 0         0  
  29         67  
250 0         0 my $args_str = join(", ", map { "$_ => $ARGS{$_}" } keys %ARGS);
  29         793028  
251 0         0  
  14         382279  
252             my $res = <<CODE;
253             class $name extends Types::Algebraic::ADT {
254 0         0 my %ARGS = ( $args_str );
  29         3744  
255             CODE
256 0         0  
  29         268  
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         131  
  29         233  
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         190  
265             }
266              
267             sub match {
268             my $self = shift;
269 0         0 for my $case (@_) {
  12         196  
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         61  
298 0         0  
  29         546  
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   309168 }
303 11         3017  
304             1;
305             __END__
306 0         0  
  12         397  
307 11         114 =encoding utf-8
  11         117  
308 11         721  
309 11     11   455698 =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