|
| 1 | +# Map with different topic variable |
| 2 | + |
| 3 | +## Preamble |
| 4 | + |
| 5 | + Author: Graham Knop <[email protected]> |
| 6 | + ID: 0023 |
| 7 | + Status: Exploratory |
| 8 | + |
| 9 | +## Abstract |
| 10 | + |
| 11 | +Allow `map` and `grep` to be given a different variable to be used as the topic |
| 12 | +variable, rather than using `$_`. Also allow multiple variables to be given to |
| 13 | +do n-at-a-time iteration. |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +Traditionally, `map` and `grep` loop over a list, aliasing `$_` to each list |
| 18 | +entry. While this can be convenient in many cases, it gets awkward when nested |
| 19 | +maps are needed. You need to manually save the `$_` value in another variable. |
| 20 | + |
| 21 | +```perl |
| 22 | +my %hash = ( |
| 23 | + foo => [ 1, 2, 3, 4 ], |
| 24 | + bar => [ 5, 6, 7, 8 ], |
| 25 | +); |
| 26 | + |
| 27 | +my @out = map { |
| 28 | + my $key = $_; |
| 29 | + map { "$key: $_" } $hash{$key}->@*; |
| 30 | +} keys %hash; |
| 31 | +``` |
| 32 | + |
| 33 | +Using `$_` can also be dangerous if you need to call code not under your |
| 34 | +direct control, due to it being the implicit target of operations like |
| 35 | +`readline`. |
| 36 | + |
| 37 | +It would be more convenient if you could use a different variable for the |
| 38 | +topic, similar to what `for` allows. |
| 39 | + |
| 40 | +```perl |
| 41 | +my @out = map my $key { |
| 42 | + map my $i { "$key: $i" } $hash{$key}->@*; |
| 43 | +} keys %hash; |
| 44 | +``` |
| 45 | + |
| 46 | +A natural extension of this syntax would be to allow n-at-a-time iteration, as |
| 47 | +`for` can do on perl 5.36+. |
| 48 | + |
| 49 | +```perl |
| 50 | +my @out = map my ($key, $val) { |
| 51 | + map my $i { "$key: $i" } $val->@*; |
| 52 | +} %hash; |
| 53 | +``` |
| 54 | + |
| 55 | +All of this would apply to `grep` as well. |
| 56 | + |
| 57 | +As the syntax proposed is currently invalid, a feature should not be needed to |
| 58 | +enable it. |
| 59 | + |
| 60 | +## Rationale |
| 61 | + |
| 62 | +The syntax chosen is meant to follow from the syntax of `for`, treating `map` |
| 63 | +as "`for` as an expression". The chosen syntax does not create any |
| 64 | +ambiguities, and naturally extends to follow the syntax used by `for` for |
| 65 | +n-at-a-time iteration. |
| 66 | + |
| 67 | +## Specification |
| 68 | + |
| 69 | +### `map my VAR BLOCK LIST` |
| 70 | + |
| 71 | +This will evaluate `BLOCK` for each element of `LIST`, aliasing `VAR` to each |
| 72 | +element. Its behavior will otherwise match `map BLOCK LIST`. It is only |
| 73 | +possible to use `my` variables for this. |
| 74 | + |
| 75 | +### `grep my VAR BLOCK LIST` |
| 76 | + |
| 77 | +This will evaluate `BLOCK` for each element of `LIST`, aliasing `VAR` to each |
| 78 | +element. Its behavior will otherwise match `grep BLOCK LIST`. It is only |
| 79 | +possible to use `my` variables for this. |
| 80 | + |
| 81 | +### `map my (VAR, VAR) BLOCK LIST` |
| 82 | + |
| 83 | +This will evaluate `BLOCK` for each set of two elements in `LIST`, aliasing |
| 84 | +the `VAR` to the first of each set, and the second `VAR` to the second. More |
| 85 | +than two variables can be used to iterate over sets of three or more items. |
| 86 | + |
| 87 | +On the last iteration, there may not be enough elements remaining to fill |
| 88 | +every `VAR` slot. In this case, a warning will be issued, and the extra `VAR` |
| 89 | +slots will be filled with `undef`. |
| 90 | + |
| 91 | +In scalar context, the return value will be the number of elements generated. |
| 92 | + |
| 93 | +### `grep my (VAR, VAR) BLOCK LIST` |
| 94 | + |
| 95 | +This will evaluate `BLOCK` for each set of two elements in `LIST`, aliasing |
| 96 | +the `VAR` to the first of each set, and the second `VAR` to the second. More |
| 97 | +than two variables can be used to iterate over sets of three or more items. |
| 98 | + |
| 99 | +On the last iteration, there may not be enough elements remaining to fill |
| 100 | +every `VAR` slot. In this case, a warning will be issued, and the extra `VAR` |
| 101 | +slots will be filled with `undef`. The extra `undef` values used will also be |
| 102 | +included in the returned list. |
| 103 | + |
| 104 | +In scalar context, this will return number of elements that would have been |
| 105 | +generated. This means it will always be a multiple of the count of `VAR` |
| 106 | +slots. |
| 107 | + |
| 108 | +## Backwards Compatibility |
| 109 | + |
| 110 | +As the syntax chosen is currently invalid, it should not present any backwards |
| 111 | +compatibility concerns. |
| 112 | + |
| 113 | +While the parsing of `map` and `grep` is complex and has ambiguities, the |
| 114 | +additions proposed are bounded and simple to parse, so they do not introduce |
| 115 | +any new ambiguities. |
| 116 | + |
| 117 | +## Security Implications |
| 118 | + |
| 119 | +None foreseen. |
| 120 | + |
| 121 | +## Examples |
| 122 | + |
| 123 | +```perl |
| 124 | +my @my_array = (11 .. 14); |
| 125 | + |
| 126 | +my @trad_map = map { $_ + 1 } @my_array; |
| 127 | +# ( 12, 13, 14, 15 ) |
| 128 | + |
| 129 | +my @new_map = map my $f { $f + 1 } @my_array; |
| 130 | +# ( 12, 13, 14, 15 ) |
| 131 | + |
| 132 | +my @pair_map = map my ($f, $g) { $f + $g } @my_array; |
| 133 | +# ( 23, 27 ) |
| 134 | + |
| 135 | +my @new_grep = grep my $f { $f > 12 } @my_array; |
| 136 | +# ( 13, 14 ) |
| 137 | + |
| 138 | +my %my_hash = ( |
| 139 | + first_key => 11, |
| 140 | + second_key => 14, |
| 141 | +); |
| 142 | + |
| 143 | +my %pair_grep = grep my ($f, $g) { $f =~ /sec/ } %my_hash; |
| 144 | +# ( second_key => 14 ) |
| 145 | +``` |
| 146 | + |
| 147 | +## Prototype Implementation |
| 148 | + |
| 149 | + - List::Util includes the functions `pairmap` and `pairgrep` which can do |
| 150 | + 2-at-a-time iteration. These use the "magic" `$a` and `$b` variables. |
| 151 | + |
| 152 | +## Future Scope |
| 153 | + |
| 154 | + - Doing n-at-a-time iteration is useful, but authors may not have a use for |
| 155 | + all of the variables, especially with `grep`. Could the extra variables be |
| 156 | + eliminated? Maybe `grep my ($key, undef) { ... } ...` could be supported? |
| 157 | + |
| 158 | + - `map`, `grep`, and even `for` could possibly be extended to allow |
| 159 | + refaliasing syntax for their variables. |
| 160 | + |
| 161 | + ```perl |
| 162 | + my %hash = ( |
| 163 | + key1 => [ 1, 2, 3 ], |
| 164 | + key2 => [ 4, 5, 6 ], |
| 165 | + ); |
| 166 | + my %out = map my ($key, \@values) { ... } %hash; |
| 167 | + ``` |
| 168 | + |
| 169 | + - Subs with a `(&@)` prototype are, in part, meant to allow mimicking the |
| 170 | + syntax of `map` and `grep`. An example of this being `List::Util::any`: |
| 171 | + |
| 172 | + ```perl |
| 173 | + use List::Util qw(any); |
| 174 | +
|
| 175 | + my $found = any { /ab/ } qw( 1234 abcd ); |
| 176 | + ``` |
| 177 | + |
| 178 | + It would be desirable to be able to to write a perl function that accepted |
| 179 | + the same syntax this this PPC proposes. |
| 180 | + |
| 181 | +## Rejected Ideas |
| 182 | + |
| 183 | + - `my` is required because the behavior of `for` when used with non-`my` or |
| 184 | + predeclared variables is confusing and hard to explain. The newer syntax |
| 185 | + `for my ($var1, $var2) { ... }` also requires `my`, both for that reason |
| 186 | + as well as implementation details. Making the `my` implicit also |
| 187 | + introduces a difference from `for` making it harder to document and |
| 188 | + explain. |
| 189 | + |
| 190 | +## Open Issues |
| 191 | + |
| 192 | +Nothing at this time. |
| 193 | + |
| 194 | +## References |
| 195 | + |
| 196 | + - Pre-PPC discussion: https://www.nntp.perl.org/group/perl.perl5.porters/2022/11/msg265104.html |
| 197 | + |
| 198 | +## Copyright |
| 199 | + |
| 200 | +Copyright (C) 2022, Graham Knop |
| 201 | + |
| 202 | +This document and code and documentation within it may be used, redistributed |
| 203 | +and/or modified under the same terms as Perl itself. |
0 commit comments