I did a presentation at RC on Functional Programming in Python and I’ve been mindful of opportunities to wrap some complicated logic/decisions into pure functions for ease of testing and reasoning ever since.

Today I tried this in a PHP codebase that I’m maintaining. Here’s a scoop of code (before the change). There’s no modularity, output is sprinkled with conditionals, and it’s hard to predict what it would do for different inputs and I can’t even fathom testing it. To make matters worse, there are a few cases in that switch and every case repeats a lot of code. I really wanted to DRY this.

$tmax = 0.5; // max fit tolerance (mm)
$tmin = 0.1; // min fit tolerance (mm)
$prec = 2; // rounding precision for mm (digits after .)
$preci = 3; // rounding precision for in (digits after .)

$wd = round($well->diameter_mm, $prec);
$wh = round($well->height_mm, $prec);
$wl = round($well->length_mm, $prec);
$ww = round($well->width_mm, $prec);

switch ($well->shape) {

    case 'round':             

        echo "<h2>Well shape and dimensions: round, $wd mm x $wh mm (" . round($wd/25.4, $preci) . "'' x " . round($wh/25.4, $preci) . "'')</h2>
              <h3>The following Pans fit this Well*:</h3>
              <table><tbody><tr><th>Pan ID:</th><th>Diameter (mm):</th><th>Height (mm):</th><th>Diameter (in):</th><th>Height (in):</th>";

        foreach ($pans as $pan) {
            $pd = round($pan->diameter_mm, $prec);
            $ph = round($pan->height_mm, $prec);
            $pdi = round($pd/25.4, $preci);
            $phi = round($ph/25.4, $preci);

            if ($pan->shape == 'round' && ($wd - $tmax) < $pd && $pd < ($wd - $tmin) && $wh > $ph) {

                echo "<tr><td>$pan->pan_no</td><td>$pd</td><td>$ph</td><td>$pdi</td><td>$phi</td></tr>";
            }

        }

        echo "</tbody></table><p>* for reference only, please contact our office for more details.</p>";
        break;

    case 'square':

        echo "<h2>Well shape and dimensions: square, $wl mm x $wh mm </h2>
              <h3>The following Pans fit this Well*:</h3>
              <table><tbody><tr><th>Pan ID:</th><th>Width (mm):</th><th>Height (mm):</th>";

        foreach ($pans as $pan) {
//and 40 more lines in the same vein

I wanted to remove the branching logic and put it into a set of functions which I can easily test. Also, my functions better be pure – no changing state or variables besides those only defined in them, no mutation of the objects passed to them, instead simply taking some values and returning new values to be used by “imperative shell”. Side note: PHP supports first-class functions and many other functional features, and I will use some of them, but it’s really about the ideas and enforcing conceptual constraints.

Here’s a set of functions I wrote. I didn’t have to write my own arrayFilter() as php implements similar functionality in array_filter(), however I noticed some quirky behavior with the indexing while using the latter and opted for more predictable option.

function arrayFilter($array, $condition) 
{
    $out = array();
    foreach ($array as $item) {
        if ($condition($item) == true) {
            $out[] = $item;
        }
    }
    return $out;
}

function nonZero($num) 
{
    return $num != 0;
}

function filterWellAndPans($well, $pans, $out_diff = 0.1, $in_diff = 0.5) 
{
    $new_pans = array();

    $well_dimensions = array();
    $well_dimensions[] = $well->length_mm;
    $well_dimensions[] = $well->width_mm;
    $well_dimensions[] = $well->diameter_mm;
    $well_dimensions = arrayFilter($well_dimensions, 'nonZero');
    $well_height = $well->height_mm;

    $new_well = (object) array(
        'shape' => $well->shape,
        'dimensions' => $well_dimensions,
        'height' => $well_height
    );

    foreach ($pans as $pan) {
        if ($pan->shape == $well->shape) {
            $pan_dimensions = array();
            $pan_dimensions[] = $pan->length_mm;
            $pan_dimensions[] = $pan->width_mm;
            $pan_dimensions[] = $pan->diameter_mm;
            $pan_dimensions = arrayFilter($pan_dimensions, 'nonZero');
            $pan_height = $pan->height_mm;

            if (count($well_dimensions) != count($pan_dimensions)) {
                throw new Exception("Dimensions number mismatch", 1);
            }
            $i = 0;
            while ($i < count($well_dimensions) 
                   && $pan_dimensions[$i] > $well_dimensions[$i] - $in_diff
                   && $pan_dimensions[$i] < $well_dimensions[$i] - $out_diff) { 
                $i++;
            }
            if ($i == count($well_dimensions) and $pan_height < $well_height) {
                $new_pans[] = (object) array(
                    'shape' => $pan->shape,
                    'dimensions' => $pan_dimensions,
                    'height' => $pan->height_mm
                );
            }
        }       
    }

    return [$new_well, $new_pans];
}

Unit testing becomes a natural thing to do. No mocks/stubs. Although I do create a few dummy objects, they’re just structs and have no behavior.

public function testNonZero()
{
    $this->assertTrue(nonZero(0.000000000000000000000000001));
    $this->assertFalse(nonZero(0.0));
}

public function testArrayFilter()
{
    $this->assertEquals(arrayFilter([], 'nonZero'), []);
    $this->assertEquals(arrayFilter([0, 0.0], 'nonZero'), []);
    $this->assertEquals(arrayFilter([3,0,4], 'nonZero'), [3,4]);
}

public function testFilterWellAndPans()
{
    $well = (object) array(
        'shape' => 'round', 
        'length_mm' => '0',
        'width_mm' => '0',
        'diameter_mm' => '59',
        'height_mm' => '6'
    );
    $pans = [(object) array(
        'shape' => 'round', 
        'length_mm' => '0',
        'width_mm' => '0',
        'diameter_mm' => '58.8',
        'height_mm' => '5'
    ), (object) array(
        'shape' => 'round', 
        'length_mm' => '0',
        'width_mm' => '0',
        'diameter_mm' => '58',
        'height_mm' => '4'

    )];

    $this->assertEquals(filterWellAndPans($well, $pans), [(object) array(
        'shape' => 'round',
        'dimensions' => ['59'],
        'height' => '6'
    ), [(object) array(
        'shape' => 'round',
        'dimensions' => ['58.8'],
        'height' => '5'
    )]]);
}

Now that I know my logic is solid, I can refactor the “imperative shell”. Here’s the code and I’m quite pleased how short and straightforward it is. (I ended up changing what information is shown on the page, too, partly because the functional approach helped me streamline my thinking about what’s important.)

$filtered = filterWellAndPans($well, $pans);
$filtered_well = $filtered[0];
$filtered_pans = $filtered[1];

$markup = '<h2>Well shape and dimensions: ' . $well->shape . ', ' 
          . implode(' mm x ', $filtered_well->dimensions) . ' mm x ' 
          . $well->height_mm . ' mm</h2>'
          . '<h3>Following pans might fit this well:</h3>'
          . '<p>(for reference only, please contact ' 
          . 'our office for more details.</p>'
          . '<table><tbody><tr><th>Pan ID:</th><th>Shape:</th>' 
          . '<th>Footprint:</th><th>Height:</th></tr>';

foreach ($filtered_pans as $pan) {
    $markup .= '<tr><td>' . $pan->pan_no . '</td><td>' . $pan->shape  
               . '</td><td>' . implode(' mm x ', $pan->dimensions) 
               . ' mm</td><td>' . $pan->height . ' mm</td></tr>';
}

$markup .= '</tbody></table>';

echo $markup;
Share →