So, I wrote a program to generate Pascal's triangle. The first ten rows of the triangle, at least. It only used simple features of Perl 6, such as scalars, nested arrays, and for
loops.
my $ELEMENTS = 10;
my @pascal = [1];
for 1 .. $ELEMENTS - 1 {
my @last = @pascal[ * - 1 ].list;
my @current;
push @current, @last[0];
for 0 .. @last - 2 {
push @current, @last[$_] + @last[$_ + 1];
}
push @current, @last[ * - 1 ];
push @pascal, [@current];
}
say @pascal.perl;
In fact, save for simple mechanically substitutable differences, it could have been a Perl 5 script. In fact, with a bit of manual array allocation, it could have been a C script. That's OK; there's a tolerance in the Perl community of writing code that looks like it was thunk in some other language.
But I've heard that Perl 6 is great at doing things with operators. For example, the Z
operator, which interleaves two lists, seems to be able to help me write my push
statements more succinctly:
my $ELEMENTS = 10;
my @pascal = [1];
for 1 .. $ELEMENTS - 1 {
my @last = @pascal[ * - 1 ].list;
my @current;
for (0, @last) Z (@last, 0) -> $left, $right {
push @current, $left + $right;
}
push @pascal, [@current];
}
say @pascal.perl;
The parentheses before and after the infix:<Z>
aren't necessary, because the Z
operator has looser precedence than comma. They're just shown here to make your eyes accustomed to reading this construct.
In fact, now that only the addition is performed in the inner loop, I might as well use the Z+
operator, which does this for me.
my $ELEMENTS = 10;
my @pascal = [1];
for 1 .. $ELEMENTS - 1 {
my @last = @pascal[ * - 1 ].list;
my @current = 0, @last Z+ @last, 0;
push @pascal, [@current];
}
say @pascal.perl;
Now as the remaining loop shrinks to a size I can take in all at once, I see a bit more clearly what I'm doing: I'm building each new list from the previous one. I could feed the previous list into a named function to get the current one:
my $ELEMENTS = 10;
my @pascal = [1];
sub next-list(@p) {
[0, @p Z+ @p, 0]
}
for 1 .. $ELEMENTS - 1 {
my @last = @pascal[ * - 1 ].list;
my @current = next-list(@last);
push @pascal, @current;
}
say @pascal.perl;
Or I could just feed it into a in-place anonymous sub.
my $ELEMENTS = 10;
my @pascal = [1];
for 1 .. $ELEMENTS - 1 {
my @last = @pascal[ * - 1 ].list;
push @pascal, (sub (@p) { [0, @p Z+ @p, 0] }).(@last);
}
say @pascal.perl;
But why even a sub? Perl 6 has a lighter construct, namely a "pointy block" (also known as a "closure" or a "lambda"). It doesn't participate in the call stack, and it's slightly easier to write.
my $ELEMENTS = 10;
my @pascal = [1];
for 1 .. $ELEMENTS - 1 {
my @last = @pascal[ * - 1 ].list;
push @pascal, (-> @p { [0, @p Z+ @p, 0] }).(@last);
}
say @pascal.perl;
Let's look at what the code does. Seed with one element. Calculate the next element based on the previous one. Stop at some point.
But that's exactly what the series operator does. The one that's written with three dots. We have a starting value, a way to get from one value to the next (our code block above), and a stopping value.
Well actually, we don't have the stopping value. But that's OK, since the series operator is lazy. So if we only request the first 10 values, it won't loop forever giving us the rest of the list.
my @pascal := do [1], -> @p { [0, @p Z+ @p, 0] } ... *;
say @pascal[^10].perl;
(The extra do
required because of a shortcoming in Rakudo.)
Now. Something very much like this code was posted first on Rosetta code and then on Moritz' blog. (TimToady used a sub, but said later that he'd have preferred binding.)
A couple of Perl 5 people's reactions were — somewhat uncharacteristically — of a negative flavour, similar to how people seem to react to the periodic table of operators:
@shadowcat_mst: an excellent example of why I consider camelia perl to be a language research project more than a production language
@pedromelo: I'm seriously considering this post as an example of what I don't want Perl6 to become...
I think these reactions are mainly feature shock. Higher-order operators, pointy blocks, and the series operator... they're all good, well-established features, which find daily use in Perl 6 programs. Maybe using them all together like that flung some people off the deep end. Never mind that the resulting script is all essential complexity, with virtually no boilerplate from the original script left.
This is the first time that's happened. I think it's important to listen to what Perl 5 people think and to try to respond to that. But I also think that this time, it's a case of them seeing some highly idiomatic Perl 6, and freaking out a bit.
And I think that that, in some odd sense, is a good thing. Well, not freaking people out, per se. But the fact that we did shows that there's something forming which might be tentatively called "idiomatic Perl 6": people on the inside can read it quite easily, but those on the outside, even Perl 5 folks looking in, instinctively go "eeeeew!".
That's OK. You're not meant to start with the idiomatic stuff. Language acquisition takes place step by step, and that goes for learning Perl 6 as well. On the way there, just don't confuse distaste with lack of familiarity.
Re: Idiomatic Perl 6
masak on 2010-08-27T09:37:00
Fixed, thanks.
Re:Fugly!
pmichaud on 2010-08-27T12:24:00
I always thought that one of Perl's philosophies was to be more like a human language. Guess I was wrong, it's math after all
... In some sense, I think it's entirely appropriate that solving a problem from (arguably) a mathematical domain ends up with a solution that looks mathematical. Just because this particular problem ends up with a mathematical-looking answer in Perl 6 doesn't mean that all Perl 6 programs will look like mathematics. Answers to problems in other domains will tend to look like the languages people in those domains use to think about them. (Quick example: parsing problems in Perl 6 tend to be solved with things like "grammar", "rule", and "token", which look very natural in that domain but would be awkward for solving Pascal's triangle.)
There's a reason that domain-specific languages exist (and not just in the realm of programming) -- it's because communication is often better served by notations other than the "lowest common denominator". In this sense, the symbolic language of mathematics is as much a "human language" as any other, and it exists because writing formulas in English just isn't all that efficient (unless you really like COBOL
:-). Pm
pascal = iterate (\row -> zipWith (+) ([0]++row) (row++[0])) [1]
The essential complexity of this example is the same. Only it contains more words than operators.
my @pascal
:= do [1], -> @p { [0, @p Z+ @p, 0] } ... *; ... (The extra do required because of a shortcoming in Rakudo.)
FWIW, I think parentheses will work here also.
my @pascal
Pm
Re:'do' required?
masak on 2010-08-27T22:44:56
Aye. I considered both forms, and went with the
do
form.When binding has list precedence, neither parentheses nor the
do
will be needed.
I really like this. I liked it even more as I worked through all of the examples to better understand them. One thing I would change:
say $_.perl for @pascal;
The output is much clearer that way (at least to me). I actually tried to get it a touch more "triangly":
my @pascal
:= do [1], -> @p { [0,@p Z+ @p,0] } ... *;
say ' ' x 2 * (10 - $_.elems), $_.perl for @pascal[^10];
But wound up with this:
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
Ah well
Re:Nifty!
masak on 2010-08-30T13:49:59
my $n = 10;
my @pascal:= do [1], -> @p { [0,@p Z+ @p,0] } ... *;
my $m = @pascal[$n - 1].perl.chars;
say (my $r = $_.perl).fmt("%{($m + $r.chars) div 2}s") for @pascal[^$n];Produces this:
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]