File Coverage

blib/lib/Types/Algebraic.pm
Criterion Covered Total %
statement 176 254 69.2
branch 9 18 50.0
condition 10 27 37.0
subroutine 39 43 90.7
pod n/a
total 234 342 68.4


line stmt bran cond sub pod time code
1             package Types::Algebraic;
2              
3 8     8   573247 use strict;
  8         76  
  8         226  
4 8     8   246 use 5.022;
  8         28  
5 8     8   47 use warnings;
  8         12  
  8         501  
6             our $VERSION = '0.05';
7              
8 8     8   59 use List::Util qw(all);
  8         30  
  8         950  
9 8     8   5108 use List::MoreUtils qw(pairwise);
  8         106365  
  8         46  
10 8     8   16761 use Keyword::Declare;
  8         944100  
  8         87  
11 8     8   7006 use Moops;
  8         144687  
  8         65  
12 8     8   503772 use PPR;
  8         19  
  8         794  
13              
14             our $_RETURN_SENTINEL = \23;
15              
16 8     8   1035312 class ADT {
  8     1   284  
  8     1   91  
  8     1   25  
  8     1   557  
  8     1   4508  
  8     1   18290  
  8     1   49  
  8     1   15543  
  8     1   21  
  8     1   77  
  8     1   1293  
  8     1   23  
  8     8   444  
  8         52  
  8         19  
  8         907  
  8         290  
  8         52  
  8         21  
  8         91  
  8         42513  
  8         21  
  8         72  
  8         8852  
  8         30932  
  8         63  
  8         431600  
  8         37  
  8         132  
  8         5365  
  8         74022  
  8         98  
  8         6976  
  8         25249  
  8         58  
  8         12976  
  8         25516  
  8         179  
  8         1236896  
  8         33  
  8         52  
  8         14  
  8         219  
  8         65  
  8         22  
  8         418  
  8         50  
  8         18  
  8         6873  
  8         38951  
  0         0  
17 8         138 has tag => (is => "ro", isa => Str);
18 8         20186 has values => (is => "ro", isa => ArrayRef);
19              
20             sub _equality {
21 20     20   58 my ($type, $x, $y) = @_;
22              
23 20 100 50     157 return 0 unless ref($x) && (ref($x) // '') eq (ref($y) // '');
      50        
      66        
24 16 100       193 return 0 unless $x->tag eq $y->tag;
25 11 50   4   71 return List::Util::all { $_ } List::MoreUtils::pairwise { $type eq '==' ? $a == $b : $a eq $b } @{$x->values}, @{$y->values};
  4         77  
  4         27  
  11         39  
  11         242  
26             }
27              
28 0     0   0 sub _equality_num { return _equality('==', @_); }
29 0     0   0 sub _equality_str { return _equality('eq', @_); }
30              
31             sub _stringify {
32 4     4   7793 my $v = shift;
33 4         39 return $v->tag . "(" . join(", ", map { "$_" } @{ $v->values }) . ")";
  0         0  
  4         23  
34             }
35              
36             use overload
37 0     0   0 '==' => sub { _equality('==', @_) },
  0         0  
38 0     4   0 '!=' => sub { ! _equality('==', @_) },
  4         16  
39 0     9   0 'eq' => sub { _equality('eq', @_) },
  9         10835  
40 0     7   0 'ne' => sub { ! _equality('eq', @_) },
  7         11065  
41 1     1   8 '""' => \&_stringify;
  1     7   4  
  1         15  
  7         61  
  7         15  
  7         102  
42             }
43              
44 8     8   247211 keytype ADTMatch is /
45             (?:
46             with (?&PerlNWS) \( (?&PerlOWS) (?<tag>(?&PerlIdentifier)) (?<identifiers> (?: (?&PerlNWS) \$ (?&PerlIdentifier) )* ) (?&PerlOWS) \) |
47             default
48             ) (?&PerlOWS) (?<block> (?&PerlBlock) ) (?&PerlOWS)
49              
50             /x;
51              
52 8     8   232679 keytype ADTConstructor is / (?<tag> (?&PerlIdentifier)) (?<fields> (?: (?&PerlNWS) : (?&PerlIdentifier) )* ) /x;
53              
54 0         0 sub import {
55 8     8   261 Moops->import;
56 8         10232  
57 0 50 50 7   0 keyword match (ParenthesesList $v, '{', ADTMatch* @body, '}') {
  0         0  
  0         0  
  8         177  
  7         513497  
  7         24  
  7         26  
58 0         0 my $res = "{\n";
  8         43  
  7         1664  
59 0         0 my $match_body = $v . "->match(\n";
  7         148  
60             for my $case (@body) {
61 0         0 my $tag = $case->{tag};
  0         0  
  0         0  
  7         151  
  7         14  
  7         18  
62 0         0 my $idents = $case->{identifiers};
  7         20  
63 0   0     0  
  7   66     191610  
64             my @idents;
65             while ($idents =~ m/ (?&PerlNWS) (?<ident> \$ (?&PerlIdentifier) $PPR::GRAMMAR )/xg ) {
66             push(@idents, $+{ident});
67             }
68              
69             my $count = scalar @idents;
70 0         0 my $args = join(", ", @idents);
  20         373972  
71             my $block = $case->{block};
72              
73 0         0 if ($tag) {
  7         196402  
74             $match_body .= "[ '$tag', $count, sub { my ($args) = \@_; $block; return \$Types::Algebraic::_RETURN_SENTINEL; } ],\n";
75 0         0 } else {
  0         0  
  7         40  
  7         24  
76 0         0 $match_body .= "[ sub { $block; return \$Types::Algebraic::_RETURN_SENTINEL; } ],\n";
  7         33  
77 0         0 }
  7         44  
78 0         0 }
  7         35  
79 0         0 $match_body .= ");\n";
  20         96  
80 0         0  
  20         69  
81             $res .= <<"EOF";
82 0         0 if (wantarray) {
  20         40  
83 0         0 my \@types_algebraic_match_result = $match_body;
  20         543806  
84 0         0 if (\@types_algebraic_match_result != 1 || \$types_algebraic_match_result[0] != \$Types::Algebraic::_RETURN_SENTINEL) { return \@types_algebraic_match_result };
  4         106551  
85             } else {
86             my \$types_algebraic_match_result = $match_body;
87 0         0 if (\$types_algebraic_match_result && \$types_algebraic_match_result != \$Types::Algebraic::_RETURN_SENTINEL) { return \$types_algebraic_match_result; }
  20         3901  
88 0         0 }
  20         124  
89 0         0 EOF
  20         159  
90             $res .= "}\n";
91 0 0       0 return $res;
  20 100       94  
92 0         0 }
  17         194  
93 8         201  
94 0 50 50 7   0 keyword data (Ident $name, '=', ADTConstructor* @constructors :sep(/\|/)) {
  0         0  
  0         0  
  0         0  
  3         43  
  8         111  
  7         380186  
  7         23  
  7         23  
95 0         0 my %ARGS;
  8         45  
  7         25  
96 0         0 for my $constructor (@constructors) {
  7         198  
97 0         0 my $tag = $constructor->{tag};
  7         31  
98 0         0  
  0         0  
  0         0  
  7         333  
  7         18  
  7         21  
99 0         0 my @args;
  0         0  
  7         194  
  7         25  
100 0   0     0 while ($constructor->{fields} =~ m/ (?&PerlNWS) : (?<ident> (?&PerlIdentifier) ) $PPR::GRAMMAR/xg ) {
  7   66     190543  
101 0         0 push(@args, $+{ident});
  19         335932  
102             }
103              
104 0         0 $ARGS{$tag} = scalar @args;
  7         185747  
105             }
106 0         0  
  7         31  
107 0         0 my $args_str = join(", ", map { "$_ => $ARGS{$_}" } keys %ARGS);
  7         23  
108 0         0  
  0         0  
  7         62  
  7         29  
109 0         0 my $res = <<CODE;
  0         0  
  7         182  
  19         365  
110 8         86 class $name extends Types::Algebraic::ADT {
  8         79  
111 0         0 my %ARGS = ( $args_str );
  8         578  
  19         47  
112 8     8   322251 CODE
  0         0  
  19         517477  
113 0         0  
  5         133298  
114             $res .= <<'CODE';
115             sub BUILD {
116 0         0 my ($self, $args) = @_;
  19         2445  
117             my $tag = $args->{tag} || confess("tag is required - please use public interface");
118             my $values = $args->{values} || confess("values is required - please use public interface");
119 0         0  
  0         0  
  7         91  
  19         142  
120             confess("Unknown constructor $tag") unless exists $ARGS{$tag};
121 0         0 confess("$tag expects $ARGS{$tag} arguments - given ".scalar @$values) unless @$values == $ARGS{$tag};
  7         105  
122             }
123              
124             sub match {
125             my $self = shift;
126 0         0 for my $case (@_) {
  7         359  
127             if (@$case == 3) {
128             my ($tag, $argc, $f) = @$case;
129             confess("$tag requires $ARGS{$tag} arguments - pattern uses $argc") unless $ARGS{$tag} == $argc;
130             if ($tag eq $self->tag) {
131             return $f->(@{ $self->values });
132             }
133             }
134             # default
135             if (@$case == 1) {
136             return $case->[0]->(@{ $self->values });
137             }
138             }
139             }
140             }
141             CODE
142              
143             for my $key (keys %ARGS) {
144             $res .= <<CODE;
145             sub $key { return $name->new( tag => '$key', values => [\@_] ); }
146             CODE
147              
148             }
149              
150             return $res;
151             }
152              
153             }
154              
155 0     0   0 sub unimport {
  7         37  
  0         0  
  0         0  
156 0 0 0     0 unkeyword data;
  19         132  
  0         0  
  0         0  
  0         0  
157 0 0 0     0 unkeyword match;
  0         0  
  0         0  
158 8     8   221362 }
  0         0  
159 8     8   517  
  8         227810  
160 8         2143 1;
161             __END__
162 0         0  
  7         213  
163 8         80 =encoding utf-8
  8         84  
164 8         342  
165 8     8   319794 =head1 NAME
166              
167             Types::Algebraic - Algebraic data types in perl
168              
169             =head1 SYNOPSIS
170              
171             use Types::Algebraic;
172              
173             data Maybe = Nothing | Just :v;
174              
175             my $sum = 0;
176             my @vs = ( Nothing, Just(5), Just(7), Nothing, Just(6) );
177             for my $v (@vs) {
178             match ($v) {
179             with (Nothing) { }
180             with (Just $v) { $sum += $v; }
181             }
182             }
183             say $sum;
184              
185             =head1 DESCRIPTION
186              
187             Types::Algebraic is an implementation of L<algebraic data types|https://en.wikipedia.org/wiki/Algebraic_data_type> in perl.
188              
189             These kinds of data types are often seen in functional languages, and allow you to create and consume structured data containers very succinctly.
190              
191             The module provides two keywords: L</"data"> for creating a new data type, and L</"match"> to provide pattern matching on the type.
192              
193             =head1 USAGE
194              
195             =head2 Creating a new type with C<data>
196              
197             The C<data> keyword is used for creating a new type.
198              
199             The code
200              
201             data Maybe = Nothing | Just :v;
202              
203             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).
204              
205             You may insantiate values of this type by using one of the constructors with the appropriate number of arguments.
206              
207             my $a = Nothing;
208             my $b = Just 5;
209              
210             =head2 Unpacking values with C<match>
211              
212             In order to access the data stored within one of these values, you can use the C<match> keyword.
213              
214             my $value = Just 7;
215             match ($value) {
216             with (Nothing) { say "There was nothing in there. :("; }
217             with (Just $v) { say "I got the value $v!"; }
218             }
219              
220             The cases are matched from the top down, and only the first matching case is run.
221              
222             You can also create a default fallback case, which will always run if reached.
223              
224             data Color = Red | Blue | Green | White | Black;
225             match ($color) {
226             with (Red) { say "Yay, you picked my favorite color!"; }
227             default { say "Bah. You clearly have no taste."; }
228             }
229              
230             =head1 LIMITATIONS
231              
232             =over 4
233              
234             =item Currently, match statements can't be nested.
235              
236             =back
237              
238             =head1 BUGS
239              
240             Please report bugs directly on L<the project's GitHub page|https://github.com/Eckankar/Types-Algebraic>.
241              
242             =head1 AUTHOR
243              
244             Sebastian Paaske Tørholm E<lt>sebbe@cpan.orgE<gt>
245              
246             =head1 COPYRIGHT
247              
248             Copyright 2020- Sebastian Paaske Tørholm
249              
250             =head1 LICENSE
251              
252             This library is free software; you can redistribute it and/or modify
253             it under the same terms as Perl itself.
254              
255             =head1 SEE ALSO
256              
257             =cut