The Art of Transduction ZendCon 2016 - Oct 19
The Art of Transduction
ZendCon 2016 - Oct 19
PHP Toolbox
The Art of Foreach
A Silly Example$grades = [98, 77, 100, 62, 90, 95, 82, 68];$sum = 0;$count = 0;foreach ($grades as $grade) { $sum += $grade; $count ++;}echo "Avg: " . $sum / $count . "\n";
A Less Silly Example
$grades = [98, 77, 100, 62, 90, 95, 82, 68];$sum = 0;foreach ($grades as $grade) { $sum += $grade;}echo "Avg: " . $sum / count($grades) . "\n";
Less Sillier
$grades = [98, 77, 100, 62, 90, 95, 82, 68];$sum = array_sum($grades);echo "Avg: " . $sum / count($grades) . "\n";
Grade Buckets$grades = [98, 77, 100, 62, 90, 95, 82, 68];$gradeBuckets = ["A" => 0, "B" => 0, "C" => 0, "F" => 0];foreach ($grades as $grade) { switch (true) { case $grade >= 90: $gradeBuckets['A']++; break; case $grade >= 80: $gradeBuckets['B']++; break; case $grade >= 70: $gradeBuckets['C']++; break; default: $gradeBuckets['F']++; }}
Phone Treefunction getNonManagerNumbers(Employee ...$employees){ $phoneNumbers = []; foreach ($employees as $employee) { if ($employee->isManager()) { continue; } $phoneNumbers[] = $employee->getPhoneNumber(); } return $phoneNumbers;}
Nav Builderfunction buildNav($links){ $html = '<ul>'; foreach ($links as $link) { $html .= '<li><a href="' . $link->getUrl() . '">' . $link->getTitle() . '</a></li>'; } $html .= '</ul>'; return $html;}
Reducefunction reduce(array $items, callable $callback, $initial){ $carryOver = $initial; foreach ($items as $item) { $carryOver = $callback( $carryOver, $item ); } return $carryOver;}
Functional Programming
• Map
• Filter
• Reduce
Grades as Reduce$avg = reduce( $grades, function ($carry, $item) { $total = $carry['count'] * $carry['avg'] + $item; $carry['count']++; $carry['avg'] = $total / $carry['count']; return $carry; }, ['count' => 0, 'avg' => 0])['avg'];
Phone Tree as Reducefunction getNonManagerNumbers($employees){ return reduce( $employees, function ($numbers, $employee) { return $employee->isManager() ? $numbers : array_merge( $numbers, [$employee->getPhoneNumber()] ); }, [] );}
Nav Builder as Reducefunction buildNav($links) { return '<ul>' . reduce( $links, function ($html, $link) { return $html . '<li><a href="' . $link->getUrl() . '">' . $link->getTitle() . '</a></li>'; } ) . '</ul>';}
What About Transducers?
What are Transducers?
Collection Pipeline
$numbers = collect($employeeService->getAllEmployees()) ->filter(function ($employee) { return ! $employee->isManager(); })->map(function ($employee) { return $employee->getPhoneNumber(); });
Installation
composer require mtdowling/transducers
Phone Tree as Transducer
use Transducers as t;$employees = (new EmployeeService)->getAllEmployees();$getNonManagerPhones = t\comp( t\filter(function ($employee) { return ! $employee->isManager(); }), t\map(function ($employee) { return $employee->getPhoneNumber(); }));$numbers = t\to_array($getNonManagerPhones, $employees);
The DataName Number Manager
Bob 303-555-1212 Yes
Sue 303-555-1234 No
Barb 303-555-1111 No
Spongebob 303-555-1001 Yes
Arnold 303-555-1313 No
Collection Data Pipeline
Name Number ManagerBob 303-555-1212 YesSue 303-555-1234 NoBarb 303-555-1111 No
Spongebob 303-555-1001 YesArnold 303-555-1313 No
Filter
Name Number ManagerSue 303-555-1234 NoBarb 303-555-1111 No
Arnold 303-555-1313 No
Map
Number303-555-1234303-555-1111303-555-1313
Transducer Data Flow
Name Number ManagerBob 303-555-1212 YesSue 303-555-1234 NoBarb 303-555-1111 No
Spongebob 303-555-1001 YesArnold 303-555-1313 No
Number303-555-1234303-555-1111303-555-1313
filtermap
NO
Transducer Data Sources
• Anything that you can use foreach on
• Arrays
• \Iterators
• Traversables
• Generators
Transducer Output• Eager
• transduce()
• into()
• to_array()
• to_assoc()
• to_string()
Transducer Output
• Lazy
• to_iter()
• xform()
• stream filters
A Bigger Example
• Incoming TSV, but should be CSV
• Date format is wrong
• Names are not capitalized
• We need days from or until birthdate, for reasons
Transformeruse transducers as t;
/* SNIP Definition of the functions used below */
$transformer = t\comp( t\drop(1), // Get rid of the header t\map($convertToArray), // Turn TSV to Array t\map($convertToDate), // Change to DateTimeImmutable Object t\map($addDaysFromBirthday), // Date math t\map($fixDateFormat), // Format DateTimeImmutable // to Y-m-d string $fixNames, // Capitalize names);
Convert TSV to Array
$convertToArray = function ($tsvRow) { $arrayRow = explode("\t", $tsvRow); $columns = ['id', 'first', 'last', 'dob']; return array_combine($columns, $arrayRow);};
What it does42 \t david \t stockton \t 1/1/1999
[ 'id' => 42, 'first' => 'david', 'last' => 'stockton', 'dob' => '1/1/1999']
Convert Date to Object$convertToDate = function ($row) { $date = DateTimeImmutable::createFromFormat( 'm/d/Y', trim($row['dob'] ) ); $row['dob'] = $date; return $row;};
Add Days from Birthday$now = new DateTimeImmutable();$thisYear = $now->format('Y');$addDaysFromBirthday = function($row) use ($now, $thisYear) { $dob = $row['dob']; $birthday = DateTimeImmutable::createFromFormat( 'Y-m-d', $dob->format("$thisYear-m-d") ); $timeUntilBirthday = $now->diff($birthday); $row['time_until_bday'] = $timeUntilBirthday->invert ? $timeUntilBirthday->format('%m months, %d days ago') : $timeUntilBirthday->format('%m months, %d days'); return $row;};
Fix Date Formatting
$fixDateFormat = function ($row) { $row['dob'] = $row['dob']->format('Y-m-d'); return $row;};
Uppercasing Names$capFirst = function ($row) { $row['first'] = ucfirst($row['first']); return $row;};
$capLast = function ($row) { $row['last'] = ucfirst($row['last']); return $row;};
Function to Build a Function
// Function to return a function$ucField = function($field) { return function ($row) use ($field) { $row[$field] = ucfirst($row[$field]); return $row; };};
Functionally Functional
$mungeField = function ($field, $munger) { return function ($row) use ($field, $munger) { $row[$field] = $munger($row[$field]); return $row; };};
Name Capitalization$fixNames = t\comp( t\map($ucField('first')), t\map($ucField('last')));$fixNamesMunge = t\comp( t\map($mungeField('first', 'ucfirst')), t\map($mungeField('last', 'ucfirst')));
Revisit Transformeruse transducers as t;
/* SNIP Definition of the functions used below */
$transformer = t\comp( t\drop(1), // Get rid of the header t\map($convertToArray), // Turn TSV to Array t\map($convertToDate), // Change to DateTimeImmutable Object t\map($addDaysFromBirthday), // Date math t\map($fixDateFormat), // Format DateTimeImmutable // to Y-m-d string $fixNames, // Capitalize names);
Where We Are
Data converted from TSV to Array
Where We Are
function array_to_csv($data){ $fh = fopen('php://temp', 'rw'); fputcsv($fh, $data); rewind($fh); $csv = stream_get_contents($fh); fclose($fh); return $csv;}
Reuse
$transformToCsv = t\comp( $transformer, t\map('array_to_csv'));
Data Source
$fh = fopen(__DIR__ . '/ZendconData.tsv', 'r');$reader = function () use ($fh) { while ($row = fgets($fh)) { yield $row; }};
Output
$write = fopen(__DIR__ . '/../data/Zendcon.csv', 'w');
TRANSFORM!!!11!
t\into($write, $reader(), $transformToCsv);
Included Transducer Functions
• map($f) - Apply $f function to each value in a collection
• filter($predicate) - If predicate returns true, retain the value, otherwise discard
• remove($predicate) - Removes items that satisfy the predicate function
• cat() - Concatenates items from nested lists
More Included Functions• partition($size) - Splits the source into arrays of the
specified size
• partition_by($predicate) - Splits input into arrays when value returned by $predicate changes
• take($n) - Takes $n items from the collection
• take_while($predicate) - Takes items from the collection while the $predicate is true
Even Moar!• take_nth($n) - Takes every $n values from the
collection
• drop($n) - Drops $n items from the start of a sequence
• drop_while($predicate) - Drops items from the collection as long as $predicate returns true
• replace(array $map) - Replaces values in the sequence according to the $map
Ermegerhd, even more?!• keep($f) - Keeps items when $f does not
return null
• keep_indexed($f) - Returns the non-null results of calling $f($index, $value)
• dedupe - Removes values that are the same as the previous value in an ordered sequence
• interpose($separator) - Adds the separator between each value in a sequence
Last list, I promise• tap($interceptor) - "Taps" into the chain, in
order to do something with the intermediate result. Does not change the sequence
• compact() - Trims out all "falsey" values from the sequence
• words() - Splits input into words
• lines() - Splits input by lines
Transducers
• Compose powerful data processing functions
• Interact with streams of data
• Easy to understand
• Simple to test
Questions?