I'm not going to wax poetic. I'm a professional Python programmer who has worked with the language for about 10 years and a former Perl 5 developer who also worked with the developers of Perl 6 for a few years.
I have no illusions about Perl 5's ... age, let's say. It was a solid tool for the day, but Python aged well and Perl 5 did not. Perl 6, on the other hand... well, it's a bag of tricks, and not all of those tricks are ones you want played on you. But, I think there are lessons here. Let's get to them.
Perl 6 is famed for its grammars (Lots of people say "rules" but that's not quite right. Perl 6 rules are just one part of the larger system of grammars which are an extension of the object space into parsing much like database ORMs are for SQL) They're great, but they also rely heavily on the way Perl 6 thinks. A native parser system in Python would be amazing, but it wouldn't be Perl 6 rules.
Also, I don't think that Python would benefit from the way Perl 6 does its exception handling or other things that Python already does, if not better, at least differently and not worse (exception handling is actually something I think Python does better).
There are also things in Perl 6 that Python doesn't do and Perl 6 doesn't do well, but I see as a herald of
things to come in Python, once a better solution is found. One great exampole is the command-line processing.
Perl 6 command-line processing is shockingly easy to use (it just introspects the MAIN
function for arguments,
types and specially formed comments then turns them into parameters). But it's horribly limited and clunky
if you want to do anything more than basic command-line flags. Good idea, incomplete execution.
The idea to gather
and take
is that take
works just like yield
, but it works on a block with a gather outside
of it, so you might
have something like this:
return [~] gather for 65..90 -> $ord_value {
take chr($ord_value);
}
Here, we concatenate ([~]
) the results of a for loop that generates the ASCII capital letters, returning the string
ABCD...WXYZ
.
This is a trivial example that could be done other ways, but it gives you the basic idea. take
just yields its value
to the nearest enclosing gather
which returns an iterator of those values.
This resolves the problem of what you see all too often in Python:
def letters():
def _letters():
for ord_value in range(65,91):
yield chr(ord_value)
return ''.join(_letters())
The issue with turning this into the Perl 6 version is that it requires something Python doesn't much like: a statement
that can contain an expression that contains a statement. This is something Python works hard to avoid, mostly because
of the issues that it causes with indentation. The easy solution, though, is to require gather
to only ever come
immediately after an assignment operator (not counting parameter assignment):
def letters():
_letters = gather for ord_value in range(65,91):
take chr(ord_value)
return ''.join(_letters())
This avoids defining a nested function which makes code harder to read and can be inserted into any scope and multiple times at that!
Again, these examples are toys and could be condensed into simple comprehensions. But that's the problem with examples: either they're too tiny to be meaningful or they're too large to focus on the thing you are trying to demonstrate.
Perl 6 uses extra bits of punctuation syntax to do a great many things that Python doesn't need or want to do that way, but the underlying ideas without the syntax are often helpful.
For example, Perl's equivalent of Python's range
is the ..
operator, but ..
is really just one of its four forms. Here they all are:
0 .. 3 # 0, 1, 2, 3
0 ..^ 3 # 0, 1, 2
0 ^.. 3 # 1, 2, 3
0 ^..^ 3 # 1, 2
And because this ends up being used so often, there is a special case:
^3 # 0, 1, 2
Now, in Python there's no need for extra punctuation. Python could simply add two new flags to range:
def range(start, stop, step=1, include_start=True, include_end=False):
...
Now those examples from above become:
range(0, 3, include_end=True)
range(0, 3)
range(0, 3, include_start=False, include_end=True)
range(0, 3, include_start=False)
I don't think that single-point range really makes much sense for Python, but if we wanted it, we could define it as a new function:
upto(3)
So the real question is why is this useful? Sure, it's cute, but does it gain us anything? And indded it does. It reduces the number of times a programmer has to decide whether to increment or decrement a value to compensate for the language's default. This means a bevy of sometimes hard to spot off-by-one errors simply go away!
"Smart matching" in Perl 6 was essentially created to enable the HLL-equicalent of a C switch that many people had been asking for in Perl 5, but which never seemed practical because HLLs deal with data so differently from their lower-level cousins. A Perl 6 switch looks like this:
given $foo {
when 1 { ... }
when 2 { ... }
default { ... }
}
But when
can take any object or expression and evaluate it in a smart-match against the topic of
the given
, which means that if your topic is a list, you can test all of its elements or a when
might take a regular expression. These when
blocks can also contain a proceed
to continue testing
rather than breaking out of the given
after a successful match.
given
assigns its topic value with the default variable, $_
which is not used as much in Perl 6
as it was in Perl 5, but does create a fascinating new bit of syntax, the bare method call:
given $foo {
when .working { say "Working" }
when .starting { say "Starting" }
default { say "Broken" }
}
These methods are called on $_
by default, which is aliased to our topic, $foo
. Indeed, there are
many benefits to thinking of methods this way, one of which is that the dot begins to look like
any other operator, and operators get assignment equivalents: foo .= bar
meaning foo = foo.bar
,
which has all of the advantages of any other operator assignment, mostly having to do with
not duplicating the left hand side value, potentially invoking side-effects.
This is probably going to be the most controversial of my points because it introduces a great many new operators into the language (well, just one... but it has many heads). Perl 6 introduced the idea of meta-operators. That is operators formed by applying some transformation to a regular operator. Reductions are the simplest version of these, and easiest to understand. Here's Python's sum operator:
sum([1,2,3,4]) # 10
But why is addition special, here? Why not have a function for every chainable operator? For example:
powertower([3,3,3]) # 7625597484987
For those unfamiliar with mathematical power-towers, they're just exponetiation, but grouped
right-associatively so, the above becomes 3 ** (3 ** 3)
, which, because Python's **
is
right associative is actually just 3 ** 3 ** 3
or 7625597484987
.
But, since the operator already knows how it works and the idea of reduction is already built in to Python, why not make this automatic:
\** [3,3,3] # 7625597484987
What's the advantage? Well, other than it being much simpler (as any expression is when you
don't have to wrap parens all the way around it) it's also almost all syntax we already know.
The associativity of **
is well documented and any beginning user of Python should know
that **
is exponentiation. What's more, this isn't just for math:
\+ [1, 2, 3] # 6
\+ ['a', 'b', 'c'] # 'abc'
\+ ['a', 2, 3] # TypeError
Notice that there's nothing going on here that's all that magical. This is just expanding to
the elements of the input sequence "joined" in a sematic sense by the +
operator. It's
extremely intuitive and easy to use.
But, I hear you say, we already have a sum builtin function. That's true... and when this is deployed
it would still be true, but just as there's no plus
builtin function, I think sum would
become deprecated eventually to avoid the duplication. Is this traumatic for long-term Python users?
Maybe. But that's not a good reason to enshrine duplication in a langauge that says there's
one way to do it.
Some other examples:
\| [...] # bitwise or of ...
\// [...] # chained integer division of ...
\== [...] # chained equality of ...
\and [...] # all(...)
\or [...] # any(...)
This is even more powerful in a language with user-defined operators, but why not skip that step and just allow functions to be reduced?
def moo($a, $b):
return $a ~ 'moo' ~ $b
\moo ['a', 'b', 'c'] # 'amoobmooc'
Sadly, there's no way to define precedence on functions, but that's perhaps something for a future Pytyhon...
Perl 5 and Perl 6 share a rather unusual feature: there are nestable versions of the quotation mark operators. Here is an example:
my $string = q{Now is the time for all good men...};
Why is this valuable? Well, try quoting this string in Python:
q{"''"}
There's nothing shocking about this. It's just a string that contains some quotes. The correct way to quote it in Python is to use tripple-single quotes:
'''"''"'''
And to be honest, I had to go back and count those carefully while typing because it's horrific.
Backslashes aren't any cleaner, really, and it's so obvious to just use a balanced operator. Python
even has the idea already of special quoting constructs that involve a prefix character. Perl, of
course, being Perl, allows anything after the q, and balances it with itself if it's not defined in
the Unicode spec as balanced (e.g. q/My string/
) or with its mate if it is (e.g. `q«My string »).
But that's not a Python-friendly thing
to do, and there's really no reason for it. Oh, and by the way, this works:
q{"'{}'"}
Perl 6 has many little things that I find to be a tremendous improvement, but which might or might not fit in Python. Some are fundamental to the language and would require, perhaps, a Python 4 to do right.
-
Loop controls like
break
take a label and labels can be placed on loops, so you can break out to an enclosing loop -
Every block is a closure. In Python, this would mean that this:
for i in range(10): yield lambda x: x + i
would actually create a new
i
closure every time, rather than returning a lambda with the samei
each time, thus making this do something it really doesn't look like it is to anyone coming from other langauges. -
Automatic accessors and semi-automatic class parameterization i.e.:
class Point { has $.x = 0; has $.y = 0; }
This is very punctuation-heavy in a Perlish way, but it's specifying that attributes
x
andy
exist on class Point, have accessors named the same and can be initialized via the new method like so:$point = Point.new(:x(10), :y(20))
and accessed like so:say $point.x
and$point.x = 20
. -
One place where Perl 6 used less punctuation than Python is in accessing metaclasses. The Perl 6 for metaclass access is just to put a
^
after the.
in method/attribute access. So Python'sfoo.__class__.__name__
in Perl 6 becomesfoo.^name
, which, for my money, is quite a bit easier to manage in my code and cleaner looking. -
The primitive pair type. Perl 6 has a datatype for the associative pair that acts as the basis for hashes (dicts in Python terms). This means that, rather than returning two-tuples, Perl 6's equivalent of items returns objects with a named
.key
and.value
. It also means that everything from named parameter passing to hash construction use the same mechanism. A Pair is constructed using the=>
operator, familiar to Perl 5 programmers. -
...
to note undefined blocks. This is something many people who see Perl 6 code online miss. They see something likesub foo {...}
and assume that this is pseudocode, but it's not. It's a perfectly valid bit of Perl 6, which gives a failure exception if the function is called. It's nearly equivalent to Python'spass
, but because it can't be successfully executed, makes it much clearer that this is "to be done" rather than intentially empty. -
Word quoting. Perl 6 uses
<...>
to surround lists of strings that are automatically quoted by breaking on whitespace. For example:<a b c d e>
which is certainly much easier to type than['a', 'b', 'c', 'd', 'e']
and thus tends to be slung around more casually in expressions. -
Named local variable passing. A Perl 6 local variable can be passed to a function which takes a named parameter where the two names are the same like so:
foo(:$bar)
. In this case, the variable $bar is passed as thebar
named parameter tofoo
. This might seem inconsequential, but how often do you see Python code littered withfoo(a=a, b=b, c=c, ...)
? -
Infinity. Having
∞
or its ascii equivalentInf
in Perl 6 makes a great deal of otherwise obtuse code quite plain. -
Block interpolation. Interpolation in general is a touchy issue in Python, but I think Perl 6 shows a path to a rational solution to the issue.
"foo: {foo.perl}."
is equivalent to the Python,"foo: {}.".format(repr(foo))
but with a significant differnence: the block inside the string is not a runtime construct. Therefore the syntax of the string includes the syntax of its subordinate block. This means you detect errors earlier and the flow of reading the code is smoother.