Rethinking Loops

John Kary  •  @johnkary

FizzBuzz

Print numbers from 1 to 100, but...
For multiples of 3 print "Fizz"
For multiples of 5 print "Buzz"
For multiples of both 3 and 5 print "FizzBuzz"


$ php fizzbuzz.php
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
...
Buzz

foreach (range(1,100) as $num) {
    $output = '';

    if (0 === $num % 3) {
        $output .= 'Fizz';
    }

    if (0 === $num % 5) {
        $output .= 'Buzz';
    }

    if (!$output) {
        $output = $num;
    }

    echo $output . "\n";
}

$max = 100;
$all = range(1, $max);
$threes = array_fill_keys(range(3, $max, 3), 'Fizz'); // [3 => 'Fizz', 6 => 'Fizz']
$fives = array_fill_keys(range(5, $max, 5), 'Buzz');  // [5 => 'Buzz', 10 => 'Buzz']
$fifteens = array_fill_keys(range(15, $max, 15), 'FizzBuzz'); // [15 => 'FizzBuzz']

echo join("\n", array_replace($all, $threes, $fives, $fifteens));
By Jeff Madsen @codebyjeff

What if we used arrays for (almost) everything?

Functional Programming


(defn encode
  [#^InputStream input #^Writer output #^String alphabet line-length]
  (let [buffer (make-array Byte/TYPE 3)]
    (loop [line 0]
      (let [len (.read input buffer)]
        (when (pos? len)
          (let [b0 (Integer/valueOf (int (aget buffer 0)))
                b1 (Integer/valueOf (int (aget buffer 1)))
                b2 (Integer/valueOf (int (aget buffer 2)))]
            (cond (= len 3)
                  (let [s0 (bit-and 0x3F (bit-shift-right b0 2))
                        s1 (bit-and 0x3F
                                    (bit-or (bit-shift-left b0 4)
                                            (bit-shift-right b1 4)))
                        s2 (bit-and 0x3F
                                    (bit-or (bit-shift-left b1 2)
                                            (bit-shift-right b2 6)))
                        s3 (bit-and 0x3F b2)]
                    (.append output (.charAt alphabet s0))
                    (.append output (.charAt alphabet s1))
                    (.append output (.charAt alphabet s2))
                    (.append output (.charAt alphabet s3))))))))))
array_map

// array_map ( callable $callback , array $array1 [, array $... ] )

$start = [1, 2, 3, 4, 5];
$end = array_map(function ($i) {
    return $i * 2;
}, $start);

// [2, 4, 6, 8, 10]
array_filter

// array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

$start = [1, 2, 3, 4, 5, 6];
$even = array_filter($start, function ($i) {
    return $i % 2 === 0;
});

// [2, 4, 6]
array_reduce

// array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )

$start = [1, 2, 3, 4, 5];
$end = array_reduce($start, function ($total, $i) {
    return $total + $i;
}, 0);

// 15

Expressive Language

Cool
  • Chess is cool
  • Roller coasters are cool
  • Playing chess on a roller coaster is cool
Cool
  • Chess is contemplative
  • Roller coasters are thrilling
  • Playing chess on a roller coaster is connivingly rebellious

Expressive Code

array_map

$start = [1, 2, 3, 4, 5];
$end = [];

for ($i=0; $i < count($start); $i++) {
    $current = $start[$i];
    $end[] = $current * 2;
}

// $end [2, 4, 6, 8, 10]

$start = [1, 2, 3, 4, 5];
$end = array_map(function ($i) {
    return $i * 2;
}, $start);

// $end [2, 4, 6, 8, 10]
array_filter

$start = [1, 2, 3, 4, 5, 6];
$even = [];

foreach ($start as $i) {
    if ($i % 2 === 0) {
        $even[] = $i;
    }
}

// $even [2, 4, 6]

$start = [1, 2, 3, 4, 5, 6];
$even = array_filter($start, function ($i) {
    return $i %2 === 0;
});

// $even [2, 4, 6]
array_reduce

$scores = [1, 2, 3, 4, 5];
$sum = 0;

foreach ($scores as $s) {
    $sum = $sum + $s;
}

// $sum === 15

$scores = [1, 2, 3, 4, 5];
$sum = array_reduce($scores, function ($end, $i) {
    return $end + $i;
}, 0);

// $sum === 15
// $sum = array_sum($start);
Users who are minors

$data = [
    ['age' => 31, 'name' => 'John'],
    ['age' => 12, 'name' => 'Jamie'],
    ['age' => 8, 'name' => 'Megan'],
];
$users = array_map(function($u) { return (object) $u; }, $data);

// array(3) {
//   [0] => class stdClass#2 (2) {
//     public $age => int(31)
//     public $name => string(4) "John"
//   }
//   [1] => class stdClass#3 (2) {
//      public $age => int(12)
//      public $name => string(5) "Jamie"
//   }
//   [2] => class stdClass#4 (2) {
//     public $age => int(8)
//     public $name => string(5) "Megan"
//   }
// }
Users who are minors

$minors = array_filter($users, function($user) {
    return $user->age < 18;
});

// [Jamie, Megan]

count($minors); // int(2)
Users who are minors

$minors = array_reduce($users, function($count, $user) {
    return $count + (int) ($user->age < 18);
});

// int(2)

What if we used arrays for (almost) everything?

Creating Users

class User {
    private $name, $email, $job;
    public function __construct($name, $email, $job) {
        $this->name = $name;
        $this->email = $email;
        $this->job = $job;
    }
}

$names = ['John', 'Mary', 'Darren'];
$emails = ['john@kary.net', 'mary@mks.com', 'darren@woodworking.com'];
$jobs = ['Pilot', 'Astronaut', 'Woodworker'];

$createUser = function ($name, $email, $job) {
    return new User($name, $email, $job);
};
$users = array_map($createUser, $names, $emails, $jobs);

// array(3) {
//   [0] =>
//   class User#2 (3) {
//     private $name => string(4) "John"
//     private $email => string(13) "john@kary.net"
//     private $job => string(5) "Pilot"
//   }
//   ...

Callables

Global functions

function sayHello($name) {
    return 'Hello ' . $name;
}

echo sayHello('John'); // "Hello John"
Static functions

class User {
    public static function sayHello($name) {
        return 'Hello ' . $name;
    }
}

echo User::sayHello("John"); // "Hello John"
Object methods

class User {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getGreeting() {
        return 'Hello ' . $this->name;
    }
}

$user = new User("John");
echo $user->getGreeting(); // "Hello John"
Anonymous functions

$greeting = function ($name) {
    return 'Hello ' . $name;
};

echo $greeting('John'); // "Hello John"
Scope

$method = "GET";

sayHello('John');

function sayHello($name) {
    return 'Hello ' . $name . ' via ' . $method;
    // PHP Notice:  Undefined variable: method
}
Scope

$method = "GET";

sayHello('John');
// "Hello John via GET"

function sayHello($name) {
    global $method;
    return 'Hello ' . $name . ' via ' . $method;
}
Scope

$method = "GET";

function sayHello($name, $verb) {
    return sprintf('Hello %s via %s', $name, $verb);
}

echo sayHello('John', $method); // "Hello John via GET"
Scope

function sayHello($name) {
    return 'Hello ' . $name;
}

sayHello('John'); // "Hello John"
echo $name;
// `PHP Notice: Undefined variable: name ...`
// because $name is not accessible in enclosing scope
Scope

$method = "GET";

$greeting = function($name) use ($method) {
    return sprintf('Hello %s via %s', $name, $method);
};

echo $greeting('John'); // "Hello John via GET"
array_* accepts many callables...

class User {
    private $name, $age;

    public static function fromArray(array $data)
    {
        $u = new User;
        $u->name = $data['name'];
        $u->age = $data['age'];
        return $u;
    }
}

$rows = [
    ['name' => 'Sarah', 'age' => 58],
    ['name' => 'Matt', 'age' => 52],
    ['name' => 'Mickey', 'age' => 42],
];
array_* accepts many callables...

$rows = [
    ['name' => 'Sarah', 'age' => 58],
    ['name' => 'Matt', 'age' => 52],
    ['name' => 'Mickey', 'age' => 42],
];

// Anonymous
$build = function(array $row) {
    return User::fromArray($row);
};
$users = array_map($build, $rows);

// Below are all equivalent
$users = array_map('User::fromArray', $rows);
$users = array_map(['User', 'fromArray'], $rows);

$u = new User();
$users = array_map([$u, 'fromArray'], $rows);

Be nice to your brain!


for ($i = 0, $for_max = count($cached_inserts); $i < $for_max; $i++) {
    if ($smarty->debugging) {
        $_params = array();
        require_once(SMARTY_CORE_DIR . 'core.get_microtime.php');
    }
    $args = unserialize($insert_args[$i]); $name = $args['name'];

    if (isset($args['script'])) {
        if(!smarty_core_get_php_resource($_params, $smarty)) {
            return false;
        }
        $php_resource = $_params['php_resource'];

        if ($resource_type == 'file') {
            $smarty->_include($php_resource, true);
        } else {
            $smarty->_eval($php_resource);
        }
    }

    $params['results'] = substr_replace($params['results'], $replace, strpos($params['results'], $cached_inserts[$i]), strlen($cached_inserts[$i]));
    if ($smarty->debugging) {
        $_params = array();
        require_once(SMARTY_CORE_DIR . 'core.get_microtime.php');
        $smarty->_smarty_debug_info[] = array('type'      => 'insert',
                                              'exec_time' => smarty_core_get_microtime($_params, $smarty) - $debug_start_time);
    }
}

return $params['results'];
for ($i = 0, $for_max = count($cached_inserts); $i < $for_max; $i++) {
function doComplicatedThing() {
    for ($i = 0, $for_max = count($cached_inserts); $i < $for_max; $i++) {
        if ($smarty->debugging) {
            $_params = array();
            require_once(SMARTY_CORE_DIR . 'core.get_microtime.php');
        }
        $args = unserialize($insert_args[$i]); $name = $args['name'];

        if (isset($args['script'])) {
            if(!smarty_core_get_php_resource($_params, $smarty)) {
                return false;
            }
            $php_resource = $_params['php_resource'];

            if ($resource_type == 'file') {
                $smarty->_include($php_resource, true);
            } else {
                $smarty->_eval($php_resource);
            }
        }

        $params['results'] = substr_replace($params['results'], $replace, strpos($params['results'], $cached_inserts[$i]), strlen($cached_inserts[$i]));
        if ($smarty->debugging) {
            $_params = array();
            require_once(SMARTY_CORE_DIR . 'core.get_microtime.php');
            $smarty->_smarty_debug_info[] = array('type'      => 'insert',
                                                  'exec_time' => smarty_core_get_microtime($_params, $smarty) - $debug_start_time);
        }
    }

    return $params['results'];
}

Working Memory

Dogs, Strips of Bacon, Gina

  • 3 dogs run into the your living room
  • Gina feeds each dog a strip of bacon
    • except Spackle because he pooped on the floor earlier
  • Spot wags her tail looking at you then rolls over
  • You're hungry and eat 2 strips of bacon from the oven set at 400°F
  • Gina hands you fresh bacon and you drop it because it's too hot
    • 5 pieces fall to the ground
    • No more bacon
  • How many bacon strips were prepared today?
  • How many bacon strips were eaten today?

Dogs, Strips of Bacon, Gina

  • 3 dogs run into the your living room
  • Gina feeds each dog 2 dogs a strip of bacon
    • except Spackle because he pooped on the floor earlier
  • Spot wags her tail looking at you then rolls over
  • You're hungry and eat 2 strips of bacon from the oven set at 400°F
  • Gina hands you more bacon and you drop it because it's too hot
    • 5 pieces fall to the ground
    • No more bacon
  • How many bacon strips were prepared today?
  • How many bacon strips were eaten today?

A program cannot change
until it is alive in a programmer's head.

–Jessica Kerr on Ruby Rogues Podcast

Nested Control Structures :(

$users = User::all();

foreach ($users as $user) {
    if (is_admin($user)) {
        echo "ID: " . $user->id . " Name: " . $user->name . "
"; foreach ($user->groups as $group) { if (!in_array($group->admin, $users)) { continue; } echo "Group Admin: " . $user->name . "
"; } } else { foreach ($user->activities as $activity) { echo "Activities: "; if ($activity->isActive()) { echo $activity->name . "
"; } } } }
Control Structure Anxiety

foreach (...) {
    if (...) {
        foreach (...) {
            if (...) {
                // ...
            }
        }
    } else {
        foreach (...) {
            if (...) {
                // ...
            }
        }
    }
}
  • New rules?
  • New variables?
  • Minor details vs New contexts
Readability :(

$customFieldValues = $this->getRequestParam("customFieldValues");
if ($customFieldValues) {
    foreach ($customFieldValues as $custValueInfo) {
        $valueEntry = $reservation->getCustomFieldValueEntry($custValueInfo['id']);
        // Not found, this is the first time they're setting the value, so we need
        // to create a new entry
        if (!$valueEntry) {
            $customField = $resource->getCustomField($custValueInfo['id']);
            if (!$customField) {
                throw new \InvalidArgumentException("Attempted to set a custom field that does not belong to the specified Resource");
            }

            $valueEntry = new ReservationCustomFieldValueEntry();
            $valueEntry->setField($customField);
            $reservation->addCustomFieldValue($valueEntry);
        }

        $valueEntry->setValue($custValueInfo['value']);
    }
}
Local variables are noisy :(

public function getOldestStudentAction(Request $request) {
    $method = $request->getMethod();
    if ($method !== 'PUT') {
        throw new \BadRequestMethod('Endpoint only accepts PUT requests');
    }

    $db = $this->getDatabase()->getConnection();
    $roster = $db->query('Roster', $request->get('roster_id'));

    $oldest = null;
    foreach ($roster->getStudents() as $student) {
        if (!$oldest || $student->getAge() > $oldest->getAge()) {
            $oldest = $student;
        }
    }

    $oldestName = $oldest->getName();
    echo "The oldest student is " . $oldestName . "\n";
    $db->close();
    $roster->updateLastAccess();

    // $method, $db, $roster, $request, $oldest, $student, $oldestName
}
Ruthlessly Eliminate Local Variables

$method = $request->getMethod();
if ($method !== 'POST') {
    // ...
}

if ($request->getMethod() !== 'POST') {
    // ...
}
Chain Method Calls

$db = $this->getDatabase();
$conn = $db->getConnection();
$roster = $conn->query('Roster', $request->get('roster_id'));

$roster = $this
    ->getDatabase()
    ->getConnection()
    ->query('Roster', $request->get('roster_id'));
array ==> map ==> filter ==> reduce

// 1. Multiply by 2
// 2. Remove any results over 9
// 3. Add numbers together

$start = [1, 2, 3, 4, 5];

echo array_reduce(array_filter(array_map(function ($i) {
    return $i * 2;
}, $start), function ($i) {
    return $i < 9;
}), function ($sum, $i) {
    return $sum + $i;
}, 0);

// "20"

array_map($fn, $array, [$...])
array_filter($array, $fn)
array_reduce($array, $fn, $initial)

$friends = ['John', 'Chris', 'Penelope', 'Megan'];

echo join(", ", addGreeting(keepUnderLength(6, $friends)));

// "Hello John, Hello Chris, Hello Megan"

$friends = ['John', 'Chris', 'Penelope', 'Megan'];

$short = keepUnderLength(6, $friends);
$greeted = addGreeting($short);
$result = join(", ", $greeted);

echo $result; // "Hello John, Hello Chris, Hello Megan"
Doug McIlroy
Inventor of Unix pipes in 1972[1]
Unix Pipes

$ cat friends.txt
John
Chris
Penelope
Megan

$ cat friends.txt | awk 'length($0) < 6'
John
Chris
Megan

$ cat friends.txt | awk 'length($0) < 6' | xargs -L1 echo "Hello"
Hello John
Hello Chris
Hello Megan

c/o Samantha Quiñones, "Building Real-Time Metric Pipelines"

Pipelines

$friends = new \Haystack\HArray(['John', 'Chris', 'Penelope', 'Megan']);
$greetings = $friends->filter(...)->map(...)->toArray();

// ["Hello John", "Hello Chris", "Hello Megan"]

class HArray extends \ArrayObject
{
    private $array = [];
    public function __construct(array $a = []) {
        $this->array = $a;
    }
    public function map(callable $func) {
        return new static(array_map($func, $this->array));
    }
    public function filter(callable $func = null) {
        return new static(array_filter($this->array, $func));
    }
    public function reduce(callable $func, $initial = null) {
        return array_reduce($this->array, $func, $initial);
    }
}
Pipelines

$friends = new \Haystack\HArray(['John', 'Chris', 'Penelope', 'Megan']);
$greetings = $friends
    ->filter(...)
    ->map(...)
    ->reduce(...)
    ->toArray();

return $greetings;
Pipelines

$friends = new \Haystack\HArray(['John', 'Chris', 'Penelope', 'Megan']);
$greetings = $friends
    ->filter(function ($name) {
        return ...
    })
    ->map(function ($name) {
        return ...
    })
    ->reduce(function ($name) {
        return ...
    })
    ->toArray();

return $greetings;

Path to Completion

Web Form Design: Filling in the Blanks
Luke Wroblewski
Pipelines

// 1. Multiply by 2
// 2. Remove any results over 9
// 3. Add numbers together

$start = [1, 2, 3, 4, 5];

echo (new \Haystack\HArray($start))
    ->map(function ($i) {
        return $i * 2;
    })
    ->filter(function ($i) {
        return $i < 9;
    })
    ->reduce(function ($sum, $i) {
        return $sum + $i;
    });

// 20

Refactor to Pipelines

Exercise: Find all classes to instantiate


$ tree /projects/loops/src
src
└── FizzBuzz
    ├── Canonical.php            class Canonical {}
    ├── Haystack.php             class Haystack {}
    └── SomeInterface.php        interface SomeInterface {}

kataProvider('FizzBuzz');
// [  "\\RethinkingLoops\\FizzBuzz\\Canonical",
//    "\\RethinkingLoops\\FizzBuzz\\Haystack" ]

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);
    $classes = [];
    foreach (scandir($src) as $filename) {
        // [  ".",
        //    "..",
        //    "Canonical.php",
        //    "Haystack.php",
        //    "SomeInterface.php" ]
    }

    return $classes;
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $classes = [];
    foreach (scandir($src) as $filename) {
        $path = sprintf('%s/%s', $src, $filename);
        // [
        //     "/projects/loops/src/FizzBuzz/.",
        //     "/projects/loops/src/FizzBuzz/..",
        //     "/projects/loops/src/FizzBuzz/Canonical.php",
        //     "/projects/loops/src/FizzBuzz/Haystack.php",
        //     "/projects/loops/src/FizzBuzz/SomeInterface.php",
        // ]
    }

    return $classes;
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $classes = [];
    foreach (scandir($src) as $filename) {
        $path = sprintf('%s/%s', $src, $filename);
        if (false === is_dir($path)) {
            // [
            //     "/projects/loops/src/FizzBuzz/Canonical.php",
            //     "/projects/loops/src/FizzBuzz/Haystack.php",
            //     "/projects/loops/src/FizzBuzz/SomeInterface.php",
            // ]
        }
    }

    return $classes;
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $classes = [];
    foreach (scandir($src) as $filename) {
        $path = sprintf('%s/%s', $src, $filename);
        if (false === is_dir($path)) {
            $path = str_replace('.php', '', $path);
            // [
            //     "/projects/loops/src/FizzBuzz/Canonical",
            //     "/projects/loops/src/FizzBuzz/Haystack",
            //     "/projects/loops/src/FizzBuzz/SomeInterface",
            // ]
        }
    }

    return $classes;
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $classes = [];
    foreach (scandir($src) as $filename) {
        $path = sprintf('%s/%s', $src, $filename);
        if (false === is_dir($path)) {
            $path = str_replace('.php', '', $path);
            $fqcn = str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $path);
            // [
            //     "\\RethinkingLoops\\FizzBuzz\\Canonical",
            //     "\\RethinkingLoops\\FizzBuzz\\Haystack",
            //     "\\RethinkingLoops\\FizzBuzz\\SomeInterface",
            // ]
        }
    }

    return $classes;
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $classes = [];
    foreach (scandir($src) as $filename) {
        $path = sprintf('%s/%s', $src, $filename);
        if (false === is_dir($path)) {
            $path = str_replace('.php', '', $path);
            $fqcn = str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $path);

            $r = new \ReflectionClass($fqcn);
            if (false === $r->isAbstract()) {
                $classes[] = $fqcn;
            }
            // [ "\\RethinkingLoops\\FizzBuzz\\Canonical",
            //   "\\RethinkingLoops\\FizzBuzz\\Haystack" ]
        }
    }

    return $classes;
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $classes = [];
    foreach (scandir($src) as $filename) {
        $path = sprintf('%s/%s', $src, $filename);
        if (false === is_dir($path)) {
            $path = str_replace('.php', '', $path);
            $fqcn = str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $path);

            $r = new \ReflectionClass($fqcn);
            if (false === $r->isAbstract()) {
                $classes[] = $fqcn;
                // $dir, $src, $classes, $filename, $path, $fqcn, $r
            }
        }
    }

    return $classes;
}

Refactor to array_*


function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    $paths = array_map(function ($filename) use ($src) {
        return sprintf('%s/%s', $src, $filename);
    }, scandir($src));

    $files = array_filter($paths, function ($path) {
        return false === is_dir($path);
    });

    $allClasses = array_map(function ($path) use ($src, $dir) {
        $path = str_replace('.php', '', $path);
        return str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $path);
    }, $files);

    return array_filter($allClasses, function ($class) {
        return false === (new \ReflectionClass($fqcn))->isAbstract();
    });
}

Refactor to Pipeline


function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    return (new \Haystack\HArray(scandir($src)))
        // [
        //    ".",
        //    "..",
        //    "Canonical.php",
        //    "Haystack.php",
        //    "SomeInterface.php",
        // ]
}

function kataProvider($dir)
{
    $src = realpath(__DIR__ . '/../src/' . $dir);

    return (new \Haystack\HArray(scandir($src)))
        ->map(function ($filename) use ($src) {
            return sprintf('%s/%s', $src, $filename);
        })
        // [
        //     "/projects/loops/src/FizzBuzz/.",
        //     "/projects/loops/src/FizzBuzz/..",
        //     "/projects/loops/src/FizzBuzz/Canonical.php",
        //     "/projects/loops/src/FizzBuzz/Haystack.php",
        //     "/projects/loops/src/FizzBuzz/SomeInterface.php",
        // ]
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    return (new \Haystack\HArray(scandir($src)))
        ->map(function ($filename) use ($src) {
            return sprintf('%s/%s', $src, $filename);
        })
        ->filter(function ($path) {
            return false === is_dir($path);
        })
        // [
        //     "/projects/loops/src/FizzBuzz/Canonical.php",
        //     "/projects/loops/src/FizzBuzz/Haystack.php",
        //     "/projects/loops/src/FizzBuzz/SomeInterface.php",
        // ]
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    return (new \Haystack\HArray(scandir($src)))
        ->map(function ($filename) use ($src) {
            return sprintf('%s/%s', $src, $filename);
        })
        ->filter(function ($path) {
            return false === is_dir($path);
        })
        ->map(function ($filepath) use ($src, $dir) {
            $filepath = str_replace('.php', '', $filepath);
            return str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $filepath);
        })
        // [ "\\RethinkingLoops\\FizzBuzz\\Canonical",
        //   "\\RethinkingLoops\\FizzBuzz\\Haystack",
        //   "\\RethinkingLoops\\FizzBuzz\\SomeInterface" ]
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    return (new \Haystack\HArray(scandir($src)))
        ->map(function ($filename) use ($src) {
            return sprintf('%s/%s', $src, $filename);
        })
        ->filter(function ($path) {
            return false === is_dir($path);
        })
        ->map(function ($filepath) use ($src, $dir) {
            $filepath = str_replace('.php', '', $filepath);
            return str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $filepath);
        })
        ->filter(function ($fqcn) {
            return false === (new \ReflectionClass($fqcn))->isAbstract();
        })
        ->toArray();
        // [ "\\RethinkingLoops\\FizzBuzz\\Canonical",
        //   "\\RethinkingLoops\\FizzBuzz\\Haystack" ]
}

function kataProvider($dir) {
    $src = realpath(__DIR__ . '/../src/' . $dir);

    return (new \Haystack\HArray(scandir($src)))
        ->map(function ($filename) use ($src) {
            return sprintf('%s/%s', $src, $filename);
        })
        ->filter(function ($path) {
            return false === is_dir($path);
        })
        ->map(function ($filepath) use ($src, $dir) {
            $filepath = str_replace('.php', '', $filepath);
            return str_replace($src . '/', '\\RethinkingLoops\\' . $dir . '\\', $filepath);
        })
        ->filter(function ($fqcn) {
            return false === (new \ReflectionClass($fqcn))->isAbstract();
        })
        ->toArray();
}
Available Today!
PHP RFC: Arrow Functions

$elements = [1, 3, 5];

// live
$result = array_map(function($x) { return $x * 2; }, $elements);
 
// proposed
$result = array_map(function($x) => $x * 2, $elements);
PHP RFC: Arrow Functions

$friends = new \Haystack\HArray(['John', 'Chris', 'Penelope', 'Megan']);

$greetings = $friends
    ->filter(function ($name) => strlen($name) < 6)
    ->map(function ($name) => 'Hello ' . $name);

echo join(", ", $greetings->toArray());
// "Hello John, Hello Chris, Hello Megan"
Rethinking Loops Playground

$ composer create-project johnkary/rethinking-loops
Reading List

(defn encode
  [#^InputStream input #^Writer output #^String alphabet line-length]
  (let [buffer (make-array Byte/TYPE 3)]
    (loop [line 0]
      (let [len (.read input buffer)]
        (when (pos? len)
          (let [b0 (Integer/valueOf (int (aget buffer 0)))
                b1 (Integer/valueOf (int (aget buffer 1)))
                b2 (Integer/valueOf (int (aget buffer 2)))]
            (cond (= len 3)
                  (let [s0 (bit-and 0x3F (bit-shift-right b0 2))
                        s1 (bit-and 0x3F
                                    (bit-or (bit-shift-left b0 4)
                                            (bit-shift-right b1 4)))
                        s2 (bit-and 0x3F
                                    (bit-or (bit-shift-left b1 2)
                                            (bit-shift-right b2 6)))
                        s3 (bit-and 0x3F b2)]
                    (.append output (.charAt alphabet s0))
                    (.append output (.charAt alphabet s1))
                    (.append output (.charAt alphabet s2))
                    (.append output (.charAt alphabet s3))))))))))

$list = [2, 4, 6, 8, 10];

for ($i=0; $i < count($list); $i++) {
    $current = $start[$i];
    $end[] = $current = *;
}

foreach ($list as $idx => $value) {
    echo $value;
    unset($list[$idx]);
}

Bugs cannot exist
in code that doesn't exist

Be Expressive

Thanks.

Source Material Credits

Questions?

John Kary  •  @johnkary

Slides: http://johnkary.net/talks
Feedback: https://joind.in/talk/342a2

Performance?

Benchmark!

:)

Why Arrays?
Quantity One Arrays
NULL null []
0 null []
1 new User() [new User()]
3 :( [
new User(),
new User(),
new User(),
]
a lot :( [...]
Why Arrays?

/** @param User[]|User|null $users */
function findUserGroups($users) {
    if ($users === null) return null;

    if ($users instanceof User) {
        $groups = Group::findByUser($users->id);
        return $groups;
    }

    if (count($users) === 0) {
        return [];
    } else {
        $groups = [];
        foreach ($users as $user) {
            $userGroups = Group::findByUser($user->id);
            if (!$groups) continue;
            $groups = array_merge($groups, $userGroups);
        }
    }

    return $groups;
}
Why Arrays?

/** @param User[]|User|null $users */
function findUserGroups($users) {
    $groups = [];

    foreach ((array) $users as $user) {
        $userGroups = Group::findByUser($user->id);
        if (!$groups) continue;
        $groups = array_merge($groups, $userGroups);
    }

    return $groups;
}
Why Arrays?

/** @param User[]|User|null $users */
function findUserGroups($users) {
    $groups = [];

    foreach ((array) $users as $user) {
        $groups = array_merge($groups, (array) Group::findByUser($user->id));
    }

    return $groups;
}
Why Arrays?

function findUserGroups($users) {
    return array_reduce((array) $users, function ($groups, $user) {
        return array_merge($groups, (array) Group::findByUser($user->id));
    }, []);
}

:)