Skip to content

Instantly share code, notes, and snippets.

@ajs
Last active June 3, 2019 14:00
Show Gist options
  • Save ajs/b16030bf18efa546178a344985671021 to your computer and use it in GitHub Desktop.
Save ajs/b16030bf18efa546178a344985671021 to your computer and use it in GitHub Desktop.
Things I think Python should learn from Perl 6

Some lessons I think Python should learn from Perl 6

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.

First the non-lessons

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.

Now to the lessons

gather/take: a better yield

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.

Not-including signifiers

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!

A switch construct

"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.

Reduction operators

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...

Nestable quotes

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{"'{}'"}

The little things

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 same i 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 and y 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's foo.__class__.__name__ in Perl 6 becomes foo.^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 like sub 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's pass, 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 the bar named parameter to foo. This might seem inconsequential, but how often do you see Python code littered with foo(a=a, b=b, c=c, ...)?

  • Infinity. Having or its ascii equivalent Inf 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment