Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Programming in PHP

Functional Programming in PHP

It’s becoming increasingly difficult to ignore the term "Functional Programming" while reading online discussions about programming languages or programming in general. People talking about FP routinely use terms that can be strange and unfamiliar to developers coming from the imperative world.

So what exactly is Functional Programming? Where does it come from? How does it differ from the programming I already know?

This talk will try to answer these questions by weaving seemingly unrelated concepts into a (hopefully) coherent story of FP, using PHP as the vehicle of expressing these ideas. The side effect (pun intended) of having a grasp of FP will make you a better developer.

Video:
https://www.youtube.com/watch?v=pWQV1x3YIJM&t=1m0s

Code:
https://github.com/pwm/fp-php-talk

Zsolt Szende

June 01, 2017
Tweet

More Decks by Zsolt Szende

Other Decks in Programming

Transcript

  1. Marc Claire Adam Nora Coffee Tea f : A !

    B A B Functions, like in mathematics
  2. Rule #1 Always return the same output for the same

    input. Rule #2 Do nothing else aka. no side effects.
  3. Turns out we do have a name for functions in

    programming. We call them pure functions
  4. // Int -> Int function increment(int $x): int { return

    $x + 1; } echo increment(2); // 3 echo increment(2); // 3 // String -> String function ask($s) { return $s.’?'; } echo ask('I am pure'); // I am pure? echo ask('I am pure'); // I am pure? Pure functions Our sets from earlier. Think of it as the function’s interface.
  5. // Int -> Int function constant($x) { return 1; }

    echo constant(4); // 1 echo constant(4); // 1 // String -> Bool function startsWithP($s) { return $s[0] === 'P'; } echo startsWithP('Pure'); // True echo startsWithP('Pure'); // True echo startsWithP('Impure'); // False echo startsWithP('Impure'); // False Pure functions
  6. Violating rule #1 // Int -> Int function f($x) {

    return random_int(0, $x); } echo f(10); // 4 echo f(10); // 7 // Int -> Int function g($x) { global $y; return $x + $y; } $y = 1; echo g(2); // 3 $y = 5; echo g(2); // 7
  7. // String -> String function f($s) { echo 'I am

    not pure anymore :('; return $s; } // String -> String function g($s) { file_put_contents( '/tmp/some.log', 'I am not pure anymore :(’ ); return $s; } Violating rule #2
  8. We are used to seeing functions written like this. f

    ( x ) = x + 1 f : x ! x + 1 But we don’t need x to be able to talk about f.
  9. f ( x ) = x + 1 x !

    x + 1 We are used to seeing functions written like this. In fact we don’t need to label it f or anything at all.
  10. f ( x ) = x + 1 x !

    x + 1 This is called a lambda expression.
  11. function f() { return function ($x) { return $x +

    1; }; } Lambdas At this point it has no label referring to it, it is just a value. f, when called, returns our \x —> x + 1 from the previous slide
  12. function f() { return function ($x) { return $x +

    1; }; } $aLabel = f(); echo $aLabel(1); // 2 Lambdas Calling f returns our lambda, which will now be bound to a label (\x —> x + 1)(1) = 1 + 1 = 2 At this point it has no label referring to it, it is just a value. f, when called, returns our \x —> x + 1 from the previous slide
  13. Lambdas function f($g, $x) { return $g($x); } echo f(function

    ($x) { return $x + 1; }, 1); // 2 Hi there, I'm a lambda being passed around…
  14. Lambda calculus $c0 = function ($s, $z) { return $z;

    }; $c1 = function ($s, $z) { return $s($z); }; $c2 = function ($s, $z) { return $s($s($z)); }; // ... $c2i = function ($n) { return $n(function ($x) { return $x + 1; }, 0); }; assert($c2i($c0) === 0); assert($c2i($c1) === 1); assert($c2i($c2) === 2); $plus = function ($m, $n) { return function ($s, $z) use ($m, $n) { return $m($s, $n($s, $z)); }; }; assert($c2i($plus($c1, $c2)) === 3); // ...
  15. Lambda calculus $true = function ($t, $f) { return $t;

    }; $false = function ($t, $f) { return $f; }; assert($true ('T', 'F') === 'T'); assert($false('T', 'F') === 'F'); $and = function ($b1, $b2) { return $b1($b2, $b1); }; $or = function ($b1, $b2) { return $b1($b1, $b2); }; assert($and($true, $true) ('T', 'F') === 'T'); assert($and($true, $false) ('T', 'F') === 'F'); assert($and($false, $true) ('T', 'F') === 'F'); assert($and($false, $false)('T', 'F') === 'F'); assert($or($true, $true) ('T', 'F') === 'T'); assert($or($true, $false) ('T', 'F') === 'T'); assert($or($false, $true) ('T', 'F') === 'T'); assert($or($false, $false)('T', 'F') === 'F'); $if = function ($p, $t, $f) { return $p($t, $f); }; assert($if($or($false, $and($true, $true)), 'T', 'F') === 'T');
  16. To save our sanity for the rest of the talk

    we will assume that we have these basic types :)
  17. Water Juice Beer Wine Alcoholic Non-Alcoholic Marc Claire Adam Nora

    Ed A B C f : A ! B g : B ! C g f : A ! C Meet little circle, pronounced “after” g f = x ! g ( f ( x ))
  18. $f = function ($s) { return 'f'.$s; }; $g =

    function ($s) { return 'g'.$s; }; echo $g($f('')); // gf echo $g($f('')); // gf echo $g($f(' !!!')); // gf !!! echo $g($f(' !!!')); // gf !!! Composition
  19. g f = x ! g ( f ( x

    )) To express “g little circle f” as a function we need to define little circle.
  20. function o($g, $f) { return function ($x) { return $g($f($x));

    }; } $f = function ($s) { return 'f'.$s; }; $g = function ($s) { return 'g'.$s; }; $gf = o($g, $f); Composition g f = x ! g ( f ( x ))
  21. function o($g, $f) { return function ($x) { return $g($f($x));

    }; } $f = function ($s) { return 'f'.$s; }; $g = function ($s) { return 'g'.$s; }; $gf = o($g, $f); echo $gf(''); // Error... Composition g f = x ! g ( f ( x ))
  22. $y = 1; function f($x) { return $x + $y;

    } echo f(1); // Undefined variable: y Closure #1 $y was declared in the outermost scope, aka. the global scope In PHP $y is not defined in this scope. PHP is lexically scoped using functions as boundaries. Inside f there's a new scope that’s local to f.
  23. function f() { $y = 1; return function ($x) {

    return $x + $y; }; } Closure #2
  24. function f() { $y = 1; return function ($x) {

    return $x + $y; }; } echo f()(1); // Undefined variable: y Closure #2 $y is now defined in the scope local to f But we don’t have access to $y inside this lambda, as it has its own scope.
  25. function f() { $y = 1; return function ($x) use

    ($y) { return $x + $y; }; } Closure #3
  26. function f() { $y = 1; return function ($x) use

    ($y) { return $x + $y; }; } echo f()(1); // 2 Closure #3 A free variable whose value gets captured when we call f. In PHP we must explicitly state what we want to capture in from the enclosing scope to achieve closure. This is done by the use() statement. Closure !
  27. function o($g, $f) { return function ($x) use ($g, $f)

    { return $g($f($x)); }; } Composition
  28. function o($g, $f) { return function ($x) use ($g, $f)

    { return $g($f($x)); }; } $f = function ($s) { return 'f'.$s; }; $g = function ($s) { return 'g'.$s; }; $gf = o($g, $f); echo $gf(''); // gf echo $gf(''); // gf echo $gf(' !!!'); // gf !!! echo $gf(' !!!'); // gf !!! Composition g f = x ! g ( f ( x ))
  29. “+” is a binary function ie. it takes 2 arguments.

    Could call it “f” instead of “+” if we wanted to. + : (N, N) ! N
  30. x ! ( y ! x + y ) Or

    is it a unary function that takes an x and returns a function, that takes an y and returns x + y?
  31. 1 2 3 x ! 1 + x x !

    2 + x x ! 3 + x ... ... + : N ! N ⇥ N N ⇥ N N
  32. Currying function normal_plus($x, $y) { return $x + $y; };

    echo normal_plus(1, 2); // 3 function curried_plus($x) { return function ($y) use ($x) { return $x + $y; }; }; echo curried_plus(1)(2); // 3
  33. function plus($x) { return function ($y) use ($x) { return

    $x + $y; }; }; $addOne = plus(1); echo $addOne(1); // 2 Currying ( y ! 1 + y)(1) = 2 ( x ! ( y ! x + y ))(1) = y ! 1 + y
  34. Currying function o($g, $f) { return function ($x) use ($g,

    $f) { return $g($f($x)); }; } function plus($x) { return function ($y) use ($x) { return $x + $y; }; }; $addTwo = o(plus(1), plus(1)); echo $addTwo(1); // 3
  35. = g ! ( f ! ( x ! g

    ( f ( x )))) Function that takes a function g, returns a function that takes a function f, returns a function that takes x and returns g(f(x)).
  36. Currying function o($g) { return function ($f) use ($g) {

    return function ($x) use ($g, $f) { return $g($f($x)); }; }; } function plus(int $x) { return function (int $y) use ($x) { return $x + $y; }; }; echo o(plus(1))(plus(1))(1); // 3 Notice how the number of captured variables grow.
  37. Currying $normalPlus = function($x, $y) { return $x + $y;

    }; $curriedPlus = curry($normalPlus); assert($normalPlus(1, 1) === $curriedPlus(1)(1)); // 2 === 2
  38. Currying function curry(callable $fn, ...$args): Closure { return function (...$partialArgs)

    use ($fn, $args) { return (function ($args) use ($fn) { return count($args) < (new ReflectionFunction($fn)) ->getNumberOfRequiredParameters() ? curry($fn, ...$args) : $fn(...$args); })(array_merge($args, $partialArgs)); }; } $normalPlus = function($x, $y) { return $x + $y; }; $curriedPlus = curry($normalPlus); assert($normalPlus(1, 1) === $curriedPlus(1)(1)); // 2 === 2 Do not panic !
  39. f g h A B C D h g :

    B ! D g f : A ! C
  40. f g h A B C D g f :

    A ! C h g : B ! D h (g f) : A ! D
  41. f g h A B C D h g :

    B ! D g f : A ! C (h g) f : A ! D
  42. f g h A B C D h (g f)=(h

    g) f Function composition is associative g f : A ! C h g : B ! D
  43. $o = curry(function ($g, $f) { return function ($x) use

    ($g, $f) { return $g($f($x)); }; }); $f = function ($s) { return 'f'.$s; }; $g = function ($s) { return 'g'.$s; }; $h = function ($s) { return 'h'.$s; }; $gf = $o($g)($f); // g o f $hg = $o($h)($g); // h o g $h_gf = $o($h)($gf); // h o (g o f) $hg_f = $o($hg)($f); // (h o g) o f // h o (g o f) = (h o g) o f assert($h_gf('') === $hg_f('')); // 'hgf' === 'hgf' Associativity of composition
  44. idA : A ! A f : A ! B

    A B idB : B ! B g : B ! A
  45. idA : A ! A f : A ! B

    A B idB : B ! B g : B ! A f idA : A ! B
  46. idA : A ! A f : A ! B

    A B idB : B ! B g : B ! A idB f : A ! B
  47. idA : A ! A f : A ! B

    A B idB : B ! B g : B ! A g idB : B ! A
  48. idA : A ! A f : A ! B

    A B idB : B ! B g : B ! A idA g : B ! A
  49. idA : A ! A f : A ! B

    A B idB : B ! B g : B ! A f idA = f = idB f g idB = g = idA g The Identity function aka. the function that does nothing
  50. $o = curry(function ($g, $f) { return function ($x) use

    ($g, $f) { return $g($f($x)); }; }); $id = function ($x) { return $x; }; $f = function ($s) { return 'f'.$s; }; $g = function ($s) { return 'g'.$s; }; // f = f o id assert($f('') === $o($f)($id)(‘’)); // 'f' === 'f' // f = id o f assert($f('') === $o($id)($f)(‘’)); // 'f' === ‘f' // g = g o id assert($g('') === $o($g)($id)(‘’)); // 'g' === 'g' // g = id o g assert($g('') === $o($id)($g)(‘’)); // 'g' === 'g' Identity of composition
  51. Map $map = curry('array_map'); $addOne = function ($x) { return

    $x + 1; }; $addOneToList = $map($addOne); print_r($addOneToList([1, 2, 3, 4, 5])); // [2, 3, 4, 5, 6]
  52. Filter Behaviour first $filter = curry(function ($f, $l) { return

    array_filter($l, $f); }); $isEven = function ($x) { return $x % 2 === 0; }; $filterEven = $filter($isEven); print_r($filterEven([1, 2, 3, 4, 5])); // [2, 4] Data (the list) last
  53. Fold $fold = curry(function ($f, $v, $l) { return array_reduce($l,

    $f, $v); }); $plus = curry(function ($x, $y) { return $x + $y; }); $sum = $fold($plus)(0); echo $sum([1, 2, 3, 4, 5]); // 15 List Operator Seed value [1, 2, 3, 4, 5] —> ((((0 + 1) + 2) + 3) + 4) + 5 = 15
  54. Right fold and Left fold $plus = curry(function ($x, $y)

    { return $x + $y; }); $sumRight = $foldr($plus)(0); $sumLeft = $foldl($plus)(0); assert($sumRight([1, 2, 3]) === $sumLeft([1, 2, 3])); // 6 = 6 ((0 + 1) + 2) + 3 = 1 + (2 + (3 + 0))
  55. Right fold and Left fold $foldr = curry(function ($f, $v,

    $l) use (&$foldr) { return count($l) > 0 ? $f($l[0])($foldr($f)($v)(array_slice($l, 1))) : $v; }); $foldl = curry(function ($f, $v, $l) use (&$foldl) { return count($l) > 0 ? $foldl($f)($f($v)($l[0]))(array_slice($l, 1)) : $v; }); $plus = curry(function ($x, $y) { return $x + $y; }); $sumRight = $foldr($plus)(0); $sumLeft = $foldl($plus)(0); assert($sumRight([1, 2, 3]) === $sumLeft([1, 2, 3])); // 6 = 6 Tiny white lie for the sake our sanity ((0 + 1) + 2) + 3 = 1 + (2 + (3 + 0))
  56. Map and Filter with Fold $map = curry(function ($f, $l)

    use ($foldr) { return $foldr(curry(function ($x, $v) use ($f) { return array_merge([$f($x)], $v); }))([])($l); }); $filter = curry(function ($p, $l) use ($foldr) { return $foldr(curry(function ($x, $v) use ($p) { return $p($x) ? array_merge([$x], $v) : $v; }))([])($l); }); $even = $filter(function ($x) { return $x % 2 === 0; }); $add1 = $map(function ($x) { return $x + 1; }); $sum = $foldr(curry(function ($x, $y) { return $x + $y; }))(0); assert($sum($add1($even([1, 2, 3, 4]))) === 8);
  57. $id = function ($x) { return $x; }; $o =

    curry(function ($g, $f) { return function ($x) use ($g, $f) { return $g($f($x)); }; }); $foldr = curry(function ($f, $v, $l) use (&$foldr) { return count($l) > 0 ? $f($l[0])($foldr($f)($v)(array_slice($l, 1))) : $v; }); Composition
  58. $id = function ($x) { return $x; }; $o =

    curry(function ($g, $f) { return function ($x) use ($g, $f) { return $g($f($x)); }; }); $foldr = curry(function ($f, $v, $l) use (&$foldr) { return count($l) > 0 ? $f($l[0])($foldr($f)($v)(array_slice($l, 1))) : $v; }); $compose = $foldr($o)($id); Composition
  59. $id = function ($x) { return $x; }; $o =

    curry(function ($g, $f) { return function ($x) use ($g, $f) { return $g($f($x)); }; }); $foldr = curry(function ($f, $v, $l) use (&$foldr) { return count($l) > 0 ? $f($l[0])($foldr($f)($v)(array_slice($l, 1))) : $v; }); $compose = $foldr($o)($id); $f = function ($s) { return 'f'.$s; }; $g = function ($s) { return 'g'.$s; }; $h = function ($s) { return 'h'.$s; }; $hgf = $compose([$h, $g, $f]); assert($hgf('') === 'hgf'); // 'hgf' === 'hgf' Composition
  60. $plus = curry(function ($x, $y) { return $x + $y;

    }); $sum = $foldr($plus)(0); echo $sum([1, 2, 3, 4, 5]); // 15 Building things #1
  61. $plus = curry(function ($x, $y) { return $x + $y;

    }); $sum = $foldr($plus)(0); echo $sum([1, 2, 3, 4, 5]); // 15 $matrix = [ [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5], ]; Building things #1
  62. $plus = curry(function ($x, $y) { return $x + $y;

    }); $sum = $foldr($plus)(0); echo $sum([1, 2, 3, 4, 5]); // 15 $matrix = [ [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5], ]; $sumMatrix = $o($sum)($map($sum)); echo $sumMatrix($matrix); // 75 Building things #1
  63. $plus = curry(function ($x, $y) { return $x + $y;

    }); $sum = $foldr($plus)(0); echo $sum([1, 2, 3, 4, 5]); // 15 $matrix = [ [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5], ]; $sumMatrix = $o($sum)($map($sum)); echo $sumMatrix($matrix); // 75 $addOneToMatrix = $map($map($plus(1))); echo $sumMatrix($addOneToMatrix($matrix)); // 100 Building things #1
  64. $explode = curry('explode'); $implode = curry('implode'); $match = curry('preg_match'); $head

    = function ($s) { return $s[0]; }; $isWord = $match('/[a-z]/i'); $initials = ??? echo $initials('This is rather cool :)'); // T I R C Building things #2
  65. $explode = curry('explode'); $implode = curry('implode'); $match = curry('preg_match'); $head

    = function ($s) { return $s[0]; }; $isWord = $match('/[a-z]/i'); $initials = $compose([ $implode(' '), $map('strtoupper'), $map($head), $filter($isWord), $explode(' ') ]); echo $initials('This is rather cool :)'); // T I R C Building things #2
  66. Building things #2 $explode = curry('explode'); $implode = curry('implode'); $match

    = curry('preg_match'); $head = function ($s) { return $s[0]; }; $isWord = $match('/[a-z]/i'); $initials = $compose([ $implode(' '), $map($o('strtoupper')($head)), $filter($isWord), $explode(' ') ]); echo $initials('This is rather cool :)'); // T I R C Optimisation for free
  67. // Imperative function initials($sentence) { $res = ''; $bits =

    explode(' ', $sentence); foreach ($bits as $bit) { if (preg_match('/[a-z]/i', $bit)) { $res .= strtoupper($bit[0]).' '; } } return trim($res); } // Functional $initials = $compose([ $implode(' '), $map($o('strtoupper')($head)), $filter($isWord), $explode(' ') ]); Imperative vs Functional
  68. • Absence of mutable state makes reasoning simple • Testing

    is trivial with pure functions • Focus on data and how it flows helps understanding • Composition is a great way to manage complexity • Computation vs effects: functional core/imperative shell Non-exhaustive pros list Non-exhaustive cons list • Requires a different mindset • More difficult to reason about performance