Skip to content

Instantly share code, notes, and snippets.

@zostay
Last active June 28, 2018 22:22
Show Gist options
  • Save zostay/88b00ca53009b6d1bc15a5aecce4f65b to your computer and use it in GitHub Desktop.
Save zostay/88b00ca53009b6d1bc15a5aecce4f65b to your computer and use it in GitHub Desktop.
Transforming Code into Beautiful, Idiomatic Perl 6

Transforming Code into Beautiful, Idiomatic P̶y̶t̶h̶o̶n̶ Perl 6

I can't help myself... I think Perl 6 is easier to read than Python... so I toook the best practices here and added Perl 6 translations instead.

README FIRST!

Okay, so I'm not really trying to one-up Python (well, maybe just a little). I think Python, especially Python 3, is a very able language and use it for a several projects. I also appreciate the work of Raymond Hettinger and Jeff Paine in putting together these best practices and summarizing them, respectively.

However, I think Perl 6 has a truly ingenious syntax that has taken 15+ years to assemble from lessons learned from 20+ years of language development in Perl preceding it and hundreds of years of experience from other languages. Perl 6 unashamedly steals language features from many others. I sincerely hope that languages will start learning from Perl 6 and stealing back (actually, I'm certain some already have). Many of these language features have been very thoughtfully debated and revised to work toward managing the trade-offs between readability, conciseness, and optimizability.

In the meantime, I wrote this gist to share some of the beauty in Perl 6 syntax and the love I have for it, of which many engineers are sadly unaware.

Cheers,
Sterling

P.S. I tried to run several of these snippets, but I have not tested them all. I will happily fix any errors you find.

Looping over a range of numbers

for i in xrange(6):
    print i**2

Better

for ^6 -> $i { say $i² }

^6 creates a range from 0 to 5 (it is a shorthand for 0..^6 where the ^ marks the end as exclusive at that end). Ranges in Perl 6 produce elements in batches as requested by the for loop. This allows us to use a value like ^6 as both a range of numbers, but in a way that can be both memory optimized and process optimized.

Looping over a collection

colors = ['red', 'green', 'blue', 'yellow']

for color in colors:
    print color

Better

my @colors = <red green blue yellow>;

for @colors -> $color {
    say $color;
}

Also note that Perl 6 provides an optimized way of making a list of strings, saving you from typing all those unnecessary quotes and commas.

Looping backwards

colors = ['red', 'green', 'blue', 'yellow']

for color in reversed(colors):
    print color

Better

for @colors.reverse -> $color {
    say $color;
}
# OR reverse @colors if you prefer; TMTOWTDI

Looping over a collection and indices

colors = ['red', 'green', 'blue', 'yellow']

for i, color in enumerate(colors):
    print i, '--->', color

Better

my @colors = <red green blue yellow>;

for @colors.kv -> $i, $color {
    say "$i ---> $color";
}

The .kv turns the list into an alternating list of indices and values. If you specify more than one parameter to the block that iterates the for-loop, the for-loop itereate N at a time, where N is the number of parameters.

Looping over two collections

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']

for name, color in izip(names, colors):
    print name, '--->', color

Better

my @names = <raymond rachel matthew>;
my @colors = <red green blue yellow>;

for @names Z @colors -> ($name, $color) {
    say "$name ---> $color";
}

The Z operator creates a new list of "tuples" from the arguments, pairwise. The new list is constructed as the for loop executes so it is both memory efficient and processor efficient.

Looping in sorted order

colors = ['red', 'green', 'blue', 'yellow']

# Forward sorted order
for color in sorted(colors):
    print colors

# Backwards sorted order
for color in sorted(colors, reverse=True):
    print colors

Better

my @colors = <red green blue yellow>;

# Forward sorted order
.say for @colors.sort;

# Backwards sorted order
.say for @colors.sort.reverse;

Calling .reverse is just as fast as going forward because it just creates an iterator that starts from the end rather than the beginning. There's no need to have a special sort reversal option.

Custom Sort Order

colors = ['red', 'green', 'blue', 'yellow']

print sorted(colors, key=len)

Better

my @colors = <red green blue yellow>;

say @colors.sort({.chars});

An invention of the Perl community, Randal Schwartz in particular, is the Schwartzian Transform. Which is a generalized solution to a common sorting problem: I want to sort based on a calculated value without having to calculate that value more than N times for a length N list. If you pass a function to the Perl 6 sort method that only accepts a single argument, the return value will be used to perform that calculation once for each element, sort the list, then return the sorted list.

For non-Perl 6 people, a couple details: First, any use of {} means that a code block is enclosed and returned. Secondly, A code block with no parameters (i.e., has no -> in front) automatically receives a single implied parameter named $_. Thirdly, a method called with no variable in front of it (e.g., .chars) causes that method to be called on the variable named $_. And fourthly, the word "length" may have any number of meanings when applied to strings, so Perl 6 avoids it and prefers .chars to count the number of characters (which generally aligns to what humans think of as "number of characters").

Call a function until a sentinel value

blocks = []
for block in iter(partial(f.read, 32), ''):
    blocks.append(block)

Better(ish)

sub iter(&f, $sentinal) {
    gather while f() -> $v {
        last if $v eq $sentinal;
        take $value;
    }
}

my @blocks = do for iter({$f.read(32)}, '') -> $block { $block }

There's no built-in short-hand for creating an iterator from an arbitrary function and a sentinel that I'm aware of, but the gather/take syntax can be used to generate arbitrary iterators, so making a function that does that is no challenge.

Distinguishing multiple exit points in loops

def find(seq, target):
    for i, value in enumerate(seq):
        if value == target:
            break
    else:
        return -1
    return i

Better

sub find($seq, $target) {
    my ($r) = gather for $seq.kv -> $i, $value {
        take $i andthen last if $value == $target;
    }
    $r // -1;
}

So, is the else the result of athe break or not? I'd always have to look it up.

Looping over dictionary keys and values

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k, v in d.iteritems():
    print k, '--->', v

Better

my %d = <matthew blue rachel green raymond red>;

for %d.kv -> $k, $v {
    say "$k ---> $v";
}

Is it just me or does Python have an obscure method to call for every optimization? Why not make you syntax sleek and simple and optimize the typical case instead? (Kudos for those that deserve them: This kind of optimization is much of what Python 3 does.)

Construct a dictionary from pairs

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']

d = dict(izip(names, colors))
# {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

For python 3: d = dict(zip(names, colors))

Better

my @names = <raymond rachel matthew>;
my @colors = <red green blue>;

my %d = @names Z=> @colors;

Remember the Z operator? If you want it to create something other than a list of tuples, you can just give it the operator you want to use to join the thing together. So, you can do a quick zip-sum of two lists with Z+, for example. In this case, we want to create a list of pairs which naturally form a Hash (basically the same as what Python calls a dict).

Looping over dictionary keys

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d:
    print k

for k in d.keys():
    if k.startswith('r'):
        del d[k]

Better

my %d = <matthew blue rachel green rayment red>;

for %d.keys -> $k {
    say $k;
}

for %d.keys -> $k {
    %d{$k}:delete if $k.starts-with('r');
}

Iterators are a wonderful thing. The regular syntax of Perl 6 basically employs them everywhere.

Counting with dictionaries

colors = ['red', 'green', 'red', 'blue', 'green', 'red']

d = {}
for color in colors:
    d[color] = d.get(color, 0) + 1

# Slightly more modern but has several caveats, better for advanced users
# who understand the intricacies
d = collections.defaultdict(int)
for color in colors:
    d[color] += 1

Better

my $d = bag(<red green red blue green red>);

A Bag in Perl 6 (or BagHash if you need one you can update later since Bag is immutable) is just a set that can count. No additional effort required.

Grouping with dictionaries -- Part I and II

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

d = collections.defaultdict(list)
for name in names:
    key = len(name)
    d[key].append(name)

Better

my @names = <raymond rachel matthew roger
             betty melissa judith charlie>
             
my %d = @names.classify({.chars});

"Grouping"? I think you mean classifying.

Clarify function calls with keyword arguments

twitter_search('@obama', retweets=False, numtweets=20, popular=True)

Better

twitter-search('@obama', :!retweets, :20numtweets, :popular);

Perl 6 adopted Ruby-style named arguments, so :!retweets is identical to retweets => False, :20numtweets is identical to numtweets => 20, and :popular is identical to popular => True.

Unpacking sequences

p = 'Raymond', 'Hettinger', 0x30, '[email protected]'

# A common approach / habit from other languages
fname = p[0]
lname = p[1]
age = p[2]
email = p[3]
fname, lname, age, email = p

Better

my @p = 'Raymond', 'Hettinger', 0x30, '[email protected]';

my ($fname, $lname, $age, $email) = @p;

Updating multiple state variables

def fibonacci(n):
    x = 0
    y = 1
    for i in range(n):
        print x
        t = y
        y = x + y
        x = t
def fibonacci(n):
    x, y = 0, 1
    for i in range(n):
        print x
        x, y = y, x + y

Better

sub fibonacci($n) {
    my ($x, $y) = (0, 1);
    for ^$n {
        say $x;
        ($x, $y) = ($y, $x + $y);
    }
}

Aren't we supposed to prefer xrange? Though, seriously, you'd never want to write that in Perl 6. Instead, do this:

sub fibonacci($n) { for (0, 1, * + * ... *)[^$n] { .say } }

Simultaneous state updates

# NOTE: The "influence" function here is just an example function, what it does 
# is not important. The important part is how to manage updating multiple 
# variables at once.
x, y, dx, dy = (x + dx * t,
                y + dy * t,
                influence(m, x, y, dx, dy, partial='x'),
                influence(m, x, y, dx, dy, partial='y'))

Better

my ($x, $y, $dx, $dy) = (
    $x + $dx * $t,
    $y + $dy * $t,
    influence($m, $x, $y, $dx, $dy, :partial<x>),
    influence($m, $x, $y, $dx, $dy, :partial<y>)
);

Concatenating strings

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

print ', '.join(names)

Better

my @names = <raymond rachel matthew roger
             betty melissa judith charlie>;
             
say @names.join(', ');
# OR, if you prefer: join ', ', @names

Updating sequences

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

del names[0]
# The below are signs you're using the wrong data structure
names.pop(0)
names.insert(0, 'mark')
names = collections.deque(['raymond', 'rachel', 'matthew', 'roger',
               'betty', 'melissa', 'judith', 'charlie'])

# More efficient with collections.deque
del names[0]
names.popleft()
names.appendleft('mark')

Better

my @names = <raymond rachel matthew roger
             betty melissa judith charlie>;
             
@names[0]:delete;
@names.shift;
@names.unshift('mark');

Using decorators to factor-out administrative logic

@cache
def web_lookup(url):
    return urllib.urlopen(url).read()

Better

use experimental :cached;
sub web-lookup($url) is cached {
    HTTP::UserAgent.new.get($url).content;
}

I have a fundamental problem with a generic cache decorator: what kind of cache you want is application specific, but the feature exists if you want it. The Perl community is somewhat divided on whether we want it or not, so it's experimental for now.

How to open and close files

with open('data.txt') as f:
    data = f.read()

Better

my $f = 'data.txt'.IO.open;
LEAVE $f.close;
my $data = $f.read();

The LEAVE phaser can be used to call any code that needs to finalize before leaving the current block.

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