Python, Perl: One-Liner Loop Functional Style

By Xah Lee. Date: . Last updated: .

Today we show a example of a loop done as a one-liner of Functional Programing style.

Problem

Suppose you have a list, and its elements are full paths to image files:

/Users/t/t4/oh/DSCN2059m-s.jpg
/Users/t/t4/oh/DSCN2062m-s.jpg
/Users/t/t4/oh/DSCN2097m-s.jpg
/Users/t/t4/oh/DSCN2099m-s.jpg
/Users/t/Icons_dir/icon_sum.gif

Note some of these has a “-s” suffix. Your job, is to generate a new list, such that, if a image file has “-s”, check on disk if there is a version without the “-s”. If so, replace the list element with that. In summary, you want to replace the “-s” ones but only if the file exist on disk. Here's a solution:

Python Solution

# -*- coding: utf-8 -*-
# python 2

import re, os.path

imgPaths=[
u'/Users/joe/lanci/t4/oh/DSCN2059m-s.jpg',
u'/Users/joe/lanci/t4/oh/DSCN2062m-s.jpg',
u'/Users/joe/lanci/t4/oh/DSCN2097m-s.jpg',
u'/Users/joe/lanci/t4/oh/DSCN2099m-s.jpg',
u'/Users/t/web/Icons_dir/icon_sum.gif']

# change the image path to the full sized image, if it exists
# that is, if image ends in -s.jpg, find one without the '-s'.
imgPaths2=[]
for myPath in imgPaths:
    p=myPath
    (dirName, fileName) = os.path.split(myPath)
    (fileBaseName, fileExtension)=os.path.splitext(fileName)
    if(fileBaseName[-2:] == '-s'):
        p2=os.path.join(dirName,fileBaseName[0:-2]) + fileExtension
        if os.path.exists(p2): p=p2
    imgPaths2.append(p)

print imgPaths2

Functional Style

But how do you do it in a functional programing style? Namely, something of the from imgPath2=f(imgPath), where the f is some function. Normally, the f would be a pure function construct made up on the spot, that is, lambda. But Python's lambda is limited to only a expression as its body. Nevertheless, one can achieve a functional style with some workaround, using map twice. Here's the code:

imgPaths3 = map( lambda x: os.path.exists(x[1]) and x[1] or x[0], \
map(lambda x: (x, re.sub( r"^(.+?)-s(\.[^.]+)$",r"\1\2", x)), imgPaths))

The first map:

newList = map(lambda x: (x, re.sub( r"^(.+?)-s(\.[^.]+)$",r"\1\2", x)), imgPaths)

generate a list of pairs (x, y), where x is the same element in imgPaths, and y is one without the -s. Then, a second map:

map( lambda x: os.path.exists(x[1]) and x[1] or x[0], newList, imgPaths))

checks if the path y exists, if so, use that, else, use the x part. The function body is essentially of a Conditional Expression, of the from (test, trueResult, falseResult). This form of a test that returns a expression is very important in functional programing. Because, in functional programing a common pattern is to sequence functions and passing values. One cannot stop in the middle and use a block structure. Here's how this form's syntax in several languages:

Boolean Operators as a Hack for Branching

In Python, there is no such form for this, but a semantically equivalent workaround is to sequence boolean expressions as used in our example. Namely:

testExpr and trueExpr or falseExpr          (Python)

This works because it exploits a particular behavior of how Python treats boolean sequences. In Python, x and y returns y if x is true. And, x or y returns x if x is true. This behavior is compatible with the mathematical sense of booleans. e.g. mathematically x and y should be true if both are true, yet in Python if x is true then y is returned, and if y is true then this is compatible with the math sense, but if y is false then the whole result is also false, so it also satisfies the math sense. Similar is the case of x or y. The point here is that one of the element is returned, instead of a real True or False value. Therefore, in a twisted way this can be used as a conditional expression.

result1= True and 4 or 5    # returns 4
result2= False and 4 or 5   # returns 5

print result1
print result2

Such language behavior is a result of the sloppiness of language design, particular seen in unix shells (For example, the operators && and ||). The problem with this design is that code relying on the returned element of a boolean sequence does not clearly indicate the programer's intention. It works by a peculiar side-effect, instead of expressed program logic.

Perl Solution

Here's the Perl code of the same loop block:

# -*- coding: utf-8 -*-
# perl

use File::Basename;

@imgPaths=(
'/Users/joe/lanci/t4/oh/DSCN2059m-s.jpg',
'/Users/joe/lanci/t4/oh/DSCN2062m-s.jpg',
'/Users/joe/lanci/t4/oh/DSCN2097m-s.jpg',
'/Users/joe/lanci/t4/oh/DSCN2099m-s.jpg',
'/Users/t/web/Icons_dir/icon_sum.gif');

# change the image path to the full sized image, if it exists
# that is, if image ends in -s.jpg, find one without the '-s'.
@imgPaths2=();
for $myPath (@imgPaths){
    $p=$myPath;
    ($fileBaseName, $dirName, $fileExtension) = fileparse($myPath, ('\.[^.]+$') );
    if (substr($fileBaseName,-2,2) eq '-s') {
        $p2= $dirName . '/' . substr($fileBaseName,0,length($fileBaseName)-2) . $fileExtension;
        print $p2, "\n";
        if (-e $p2) { $p=$p2}
    }
    push(@imgPaths2, $p);
}

use Data::Dumper;
print Dumper(\@imgPaths2)

In Perl, this can be written in a functional style one-liner:

@imgPaths3= map{
    $y = $_;
    $y =~ s/^(.+?)-s(\.[^.]+)$/$1$2/;
    -e $y ? $y : $_;
    } @imgPaths;

perldoc -f map