Harry:

(Expect typographical errors in such a long post...)

tl;dr? RTFM.

On Fri, Jan 09, 2015 at 08:26:55PM -0500, Harry Putnam wrote:
> I'm taxing peoples patience I suppose but being considerably thick of
> skull I cannot just look at this and see what it does.
> 
> > my @files = map { $_->[0] }
> >                    sort { $a cmp $b }
> >                    map {[$_, (stat("$dir/$_"))[9] ] }
> >                    grep { ! /^\./ && -f "$dir/$_" } readdir($dh);
> 
> my @files = map { $_->[0] }  # What happened there?
> 
>    sort { $a cmp $b } # I now this sorts stuff, but not sure how or
>                       # what kind of sort
> 
> perldoc -f cmp  Tells you only to find the meaning in perlop
> 
> perldoc perlop - /cmp
> 
> Looking at every appearance of `cmp'... I still do not know what the
> heck it does.  I did find out that it returns 1 or 0.  Seems kind of
> thin to help me understand what it does.
> 
>  map {[$_, (stat("$dir/$_"))[9] ] } ## I see a stat is done and
>                                     # modtime extracted what else
>                                     # happens here?

You're better off looking at a statement like this in reverse, or
at least in the "order of operations".

You obviously need to understand Perl syntax to understand that
so make sure you go through `perldoc perlsyn' as a starting
point, and I'm guessing `perldoc perlfunc' may be needed as well.
You will need to understand context too. Really I recommend any
Perl newbie work their way through the core perldocs until they
can read code without being confused anymore.

In the case of this:

    my @files = map { $_->[0] }
                       sort { $a cmp $b }
                       map {[$_, (stat("$dir/$_"))[9] ] }
                       grep { ! /^\./ && -f "$dir/$_" } readdir($dh);

An experienced Perl programmer would easily see these as a chain
of core function calls where arguments for each function occur
after its name (so the result of subsequent function calls is fed
into earlier ones when reading the statement left-to-right).

It takes advantage of being able to call subroutines without
parenthesis and of being able to pass a lexical block directly to
a subroutine. The perldocs explain the details of how all of that
works. If you don't know yet then you'll probably have to read
the docs and experiment to get a handle on it.

                   grep { ! /^\./ && -f "$dir/$_" } readdir($dh);

Initially this a call to readdir(). It is in list context so
readdir is going to read all remaining entries from the directory
handle $dh. Those will be passed into grep(). Grep is one of
those special subroutines that is sort of like an operator too
(you'll see in a minute that we're using a bunch of these).

    { ! /^\./ && -f "$dir/$_" }

This is a lexical blocK that is being passed to grep. The first
argument to grep is effectively code AKA a set of Perl statements
AKA just like a function. This is sometimes referred to as
"functional form" or a "higher order function". It is used by
grep to filter the list that follows down to matching elements.
The default variable, $_, is aliased to each element of the list
before executing the code block. It could be writen a couple of
other ways:

    sub my_filter { return ! /^\./ && -f "$dir/$_"i; }

    grep(\&my_filter, readdir($dh));

Or...

    grep(sub { ! /^\./ && -f "$dir/$_"i }, readdir($dh));

Or...

    grep ! /^\./ && -f "$dir/$_", readdir($dh);

Which one is best is mostly a matter of opinion. The first one is
really quite understandable, but requires quite a bit more code
and also pollutes the package namespace with a subroutine.
There's nothing wrong with that, but it may not be desirable. The
second one is practically the same, except that it avoids naming
the function and instead passes a reference to an anonymous
function (I also left out the explicit return keyword and
semi-colon, which are both optional in this case, though some
would prefer they were always used). The last one uses an
expression as the code argument (so no explicit block or function
is defined), but it still means the same thing.

I often prefer the last form, but depending on complexity it can
become overwhelming or even impossible. The original form is
often preferred. I tend to slide form expression, to block, to
named subroutine reference depending on complexity.

The code being passed to grep tests that the item doesn't begin
with a dot (i.e., it's not a hidden file in UNIX) and it is a
regular file. A more explicit way to say this could be:

    sub my_filter {
        my $is_dot_file = $_ =~ /^\./;
        my $is_reg_file = -f "$dir/$_";

        return ! $is_dot_file && $is_reg_file;
    }

So the grep will reduce the list of files from readdir($dh) down
to regular files that aren't hidden. Next up we have:

                       map {[$_, (stat("$dir/$_"))[9] ] }

This is another special function, map(), that is sort of like an
operator too. Again it is being called without explicit
parenthesis for the arguments and is being passed code as a block
which against aliases $_ to the elements of the list. Again we
have list context and it also takes the filtered list that grep
returned. While grep is used to filter the list, map is used to
transform it. Another way to say it would be:

    sub my_filter {
        my $is_dot_file = $_ =~ /^\./;
        my $is_reg_file = -f "$dir/$_";

        return ! $is_dot_file && $is_reg_file;
    }

    sub my_mapper {
        my @stats = stat("$dir/$_");
        my $mtime = $stats[9];
        my $tuple = [$_, $mtime];

        return $tuple;
    }

    my @files = readdir($dh);
    my @reg_files = grep \&my_filter, @files;

    map(\&my_mapper, @reg_files);

The code block passed to map is returning a tuple (a pair),
represented with an anonymous array, containing the name of the
file in its first element and its last modified timestamp (mtime)
in the its second element. A reference to the array representing
the tuple for each regular file is returned for each element in
the list. A list of these tuples or array references is what map
returns.

                       sort { $a cmp $b }

Next up we have sort(). Just like grep and map it is being called
without parenthesis, is accepting code as a block, and is
accepting the list of previously filtered and mapped files to
work on. Sort uses the code block to compare two elements within
the list at a time. It does this to determine the order in which
they should be sorted. The elements are aliased to $a and $b
within the block (so you should never use those names in your own
code).

If the block returns a negative number then it indicates that $a
comes before $b. If the block returns a positive number then it
indicates that $a comes after $b (in other words, $b comes before
$a). If it returns zero then it indicates that $a and $b are
considered equal for the purposes of the sort. Two core operators
that do this are <=> and cmp. <=> works with numbers and cmp
works with strings. So this sort is sorting stringwise in
ascending order (strings that are considered "less" come before
strings that are considered "more"). A stringwise sort is
typically based on a position comparison between the character
ordinals in the string.

That leaves us with this:

    sub my_filter {
        my $is_dot_file = $_ =~ /^\./;
        my $is_reg_file = -f "$dir/$_";

        return ! $is_dot_file && $is_reg_file;
    }

    sub my_mapper {
        my @stats = stat("$dir/$_");
        my $mtime = $stats[9];
        my $tuple = [$_, $mtime];

        return $tuple;
    }

    sub compare_files {
        return $a <=> $b;
    }

    my @files = readdir($dh);
    my @reg_files = grep \&my_filter, @files;
    my @file_tuples = map(\&my_mapper, @reg_files);

    sort \&compare_files, @file_tuples;

That won't quite work as we intended because we'd be comparing
the array references of our tuples. Dermot probably meant to
compare the mtime within the tuples. We can fix that easily
enough:

    sub compare_mtimes {
        my $lhs = $a->[1];
        my $rhs = $b->[1];

        return $lhs <=> $rhs;
    }

    sort \&compare_mtimes, @file_tuples;

Finally we have:

    my @files = map { $_->[0] }

Here we have another invocation of map() which again is getting
the results from the previous function call chain of grep, map,
and sort. We've already gone over what map does so we should be
able to figure out what this is doing already. Taking our sorted
list of regular file tuples and mapping them back to a list of
file names.

    sub my_filter {
        my $is_dot_file = $_ =~ /^\./;
        my $is_reg_file = -f "$dir/$_";

        return ! $is_dot_file && $is_reg_file;
    }

    sub my_mapper {
        my @stats = stat("$dir/$_");
        my $mtime = $stats[9];
        my $tuple = [$_, $mtime];

        return $tuple;
    }

    sub compare_mtimes {
        my $lhs = $a->[1];
        my $rhs = $b->[1];

        return $lhs <=> $rhs;
    }

    sub extract_filename {
        my $filename = $_->[0];

        return $filename;
    }

    my @files = readdir($dh);
    my @reg_files = grep \&my_filter, @files;
    my @file_tuples = map(\&my_mapper, @reg_files);
    my @sorted_tuples = sort \&compare_mtimes, @file_tuples;
    my @sorted_files = map \&extract_filenames, @sorted_tuples;

And there you have it. A very verbose way to write this:

    my @files = map $_->[0],
            sort { $a->[1] <=> $b->[1] }
            map [$_, (stat("$dir/$_"))[9]],
            grep !/^\./ && -f "$dir/$_",
            readdir($dh);

Or put another way, this is a very terse way to write that.

I hope this wall of text helps. For the most part, you just need
to spend some quality time with The Revelant Manuals (TFM).

Regards,


-- 
Brandon McCaig <bamcc...@gmail.com> <bamcc...@castopulence.org>
Castopulence Software <https://www.castopulence.org/>
Blog <http://www.bambams.ca/>
perl -E '$_=q{V zrna gur orfg jvgu jung V fnl. }.
q{Vg qbrfa'\''g nyjnlf fbhaq gung jnl.};
tr/A-Ma-mN-Zn-z/N-Zn-zA-Ma-m/;say'

Attachment: signature.asc
Description: Digital signature

Reply via email to