Functional Programming with PHP
What is functional programming?
An alternative to procedural
or
object-oriented programming
Functions
are the
building block
Functional Programming
- minimizes side-effects
- avoids mutation
- encourages explicit inputs and outputs
Why?
- easy to think about in isolation
- easier to test
- higher level of abstraction
- easier to refactor
The basics
map
array_map(
function($x) {
return 2 * $x;
},
[1, 2, 3])
/* returns
Array
(
[0] => 2
[1] => 4
[2] => 6
)
*/
$purchases = [
[
'name' => 'hover board',
'price' => 10000
],
[
'name' => 'light saber',
'price' => 100000
],
[
'name' => 'Rocket Cat',
'price' => 1000000
]
];
setlocale(LC_MONETARY, 'en_US');
array_map(
function($purchase) {
$purchase['formatted_price'] =
money_format(
'$%!i',
$purchase['price']);
return $purchase;
},
$purchases);
Array
(
[0] => Array
(
[name] => hover board
[price] => 10000
[formatted_price] => $10,000.00
)
[1] => Array
(
[name] => light saber
[price] => 100000
[formatted_price] => $100,000.00
)
[2] => Array
(
[name] => Rocket Cat
[price] => 1000000
[formatted_price] => $1,000,000.00
)
)
filter
array_filter(
[1, 2, 3, 4],
function($x) {
return $x % 2 === 0;
})
/* returns
Array
(
[1] => 2
[2] => 4
)
*/
$customers = [
[
'name' => 'Harley Quinn',
'member' => true
],
[
'name' => 'Bruce Wayne',
'member' => false
],
[
'name' => 'Tim Drake',
'member' => true
]
];
$customers_to_email =
array_filter(
$customers,
function($customer) {
return
$customer['member'] === true;
});
Array
(
[0] => Array
(
[name] => Harley Quinn
[member] => 1
)
[2] => Array
(
[name] => Tim Drake
[member] => 1
)
)
reduce
array_reduce(
[1, 2, 3],
function($a, $x) {
return $a + $x;
},
0)
// returns 6
$heroes = [
[
'name' => 'Batman',
'species' => 'Human'
],
[
'name' => 'Night Wing',
'species' => 'Human'
],
[
'name' => 'Superman',
'species' => 'Kryptonian'
],
[
'name' => 'Supergirl',
'species' => 'Kryptonian'
],
];
$heroes_by_species =
array_reduce(
$heroes,
function($heroes_by_species, $hero) {
$species = $hero['species'];
$heroes_by_species[$species] =
$heroes_by_species[$species]
? $heroes_by_species[$species]
: [];
array_push(
$heroes_by_species[$species],
$hero['name']);
return $heroes_by_species;
},
[]);
Array
(
[Human] => Array
(
[0] => Batman
[1] => Night Wing
)
[Kryptonian] => Array
(
[0] => Superman
[1] => Supergirl
)
)
currying
$f = function ($x, $y, $z) {
return $x + $y + $z;
};
$f(1, 2, 3); //returns 6
$g = curry($f);
$g(1, 2, 3); //returns 6
$h = $g(1);
$h(2, 3); //returns 6
$j = $h(2);
$j(3); //returns 6
class curry
{
private $f;
private $args;
private $count;
public function __construct($f, $args = [])
{
if ($f instanceof curry) {
$this->f = $f->f;
$this->count = $f->count;
$this->args = array_merge($f->args, $args);
}
else {
$this->f = $f;
$this->count = count((new ReflectionFunction($f))->getParameters());
$this->args = $args;
}
}
public function __invoke()
{
if (count($this->args) + func_num_args() < $this->count) {
return new curry($this, func_get_args());
}
else {
$args = array_merge($this->args, func_get_args());
$r = call_user_func_array($this->f, array_splice($args, 0, $this->count));
return is_callable($r) ? call_user_func(new curry($r, $args)) : $r;
}
}
}
function curry($f)
{
return new curry($f);
}
http://stackoverflow.com/questions/1609985/
is-it-possible-to-curry-method-calls-in-php
pipe
$f = function ($x) {
return 2 * $x;
};
$g = function ($x) {
return $x + 3;
};
$h = function ($x) {
return $x * $x;
};
pipe($f, $g, $h)(3); // returns 81
function pipe() {
$functions = func_get_args();
return function ($x) use($functions) {
$result =
call_user_func_array(
$functions[0],
func_get_args());
return array_reduce(
array_slice($functions, 1),
function ($result, $func) {
return $func($result);
},
$result);
};
}
Putting it all together
$map = $curry('array_map');
$filter = $curry(function ($func, $arr) {
return array_filter($arr, $func);
});
$reduce = $curry(function ($func, $acc, $arr) {
return array_reduce($arr, $func, $acc);
});
$double =
$map(function ($x) {
return 2 * $x;
});
$evens =
$filter(function ($x) {
return $x % 2 === 0;
});
$sum =
$reduce(function ($a, $x) {
return $a + $x;
},
0);
print_r($double([1, 2, 3]));
print_r($evens([1, 2, 3, 4]));
print_r($sum([1, 2, 3]));
Array
(
[0] => 2
[1] => 4
[2] => 6
)
Array
(
[1] => 2
[3] => 4
)
6
Comma separate a string of numbers
$split_every = $curry(function ($every, $string) {
return str_split($string, $every);
});
$reverse = $curry(function ($x) {
if(is_string($x)) {
return strrev($x);
}
return array_reverse($x);
});
$implode = $curry('implode');
//Having to use "use" is a bit annoying...
$comma_separate = $curry(function ($width, $number)
use(
$pipe,
$reverse,
$split_every,
$reverse,
$map,
$implode) {
return $pipe(
$reverse,
$split_every($width),
$reverse,
$map($reverse),
$implode(','))
($number);
});
$comma_separate(3, '100000000');
// returns 100,000,000
format dollars
$to_string = function ($x) {
return (string) $x;
};
$concat = $curry(function ($str1, $str2) {
return $str1.$str2;
});
$format_dollars =
$pipe(
$to_string,
$comma_separate(3),
$concat('$'));
$format_dollars(100000000);
//returns $100,000,000
format
dollars and cents
$explode = $curry(function ($delimeter, $string) {
return explode($delimeter, $string);
});
$format_dollars_cents =
$pipe(
$to_string,
$explode('.'),
function ($arr) use ($format_dollars) {
return [
$format_dollars($arr[0]),
array_key_exists(1, $arr)
? mb_strimwidth($arr[1], 0, 2)
: '00'
];
},
$implode('.'));
$format_dollars_cents(100000000.2321)
//returns $100,000,000.23
$format_dollars_cents(100000000)
//returns $100,000,000.00
Getting values
from an array
$get = $curry(function ($key, $arr) {
return $arr[$key];
});
$hero = [
'name' => 'spider-man',
'primary_gadget' => 'web shooters'
];
$get_primary_gadget = $get('primary_gadget');
$get_primary_gadget($hero)
// returns 'web shooters'
"Setting" array values
$purchase = [
'name' => 'hoverboard',
'price' => 10000
];
$set('price', 20000, $purchase);
/* returns
Array
(
[name] => hoverboard
[price] => 20000
)
*/
$set = $curry(function ($key, $value, $arr) {
$new_arr = array_replace_recursive([], $arr);
$new_arr[$key] = $value;
return $new_arr;
});
over
$format_purchase_price =
$over(
$get('price'),
$set('formatted_price'),
$format_dollars_cents);
$purchase = [
'name' => 'hoverboard',
'price' => 30000
];
$format_purchase_price($purchase);
/* returns
Array
(
[name] => hoverboard
[price] => 30000
[formatted_price] => $30,000.00
)
*/
$over = $curry(function($getter, $setter, $func, $arr) {
return $setter($func($getter($arr)), $arr);
});
debugging
$broken_comma_separate = $curry(function ($width, $number)
use(
$pipe,
$reverse,
$split_every,
$reverse,
$map,
$implode) {
return $pipe(
$reverse,
$split_every($width),
$reverse,
$implode(','))
($number);
});
$broken_comma_separate(3, '123123123');
//outputs '321,321,321', what gives?
$tap = function ($x) {
print_r($x);
return $x;
};
$broken_comma_separate = $curry(function ($width, $number)
use(
$pipe,
$reverse,
$split_every,
$reverse,
$map,
$implode,
$tap) {
return $pipe(
$reverse,
$split_every($width),
$reverse,
$tap,
$implode(','))
($number);
});
/* prints
Array
(
[0] => 321
[1] => 321
[2] => 321
)
Ah, we forgot to reverse each array element. :) */
$comma_separate = $curry(function ($width, $number)
use(
$pipe,
$reverse,
$split_every,
$reverse,
$map,
$implode) {
return $pipe(
$reverse,
$split_every($width),
$reverse,
$map($reverse),
$implode(','))
($number);
});
Easy to test
$add_2 = function ($a, $b) {
return $a + $b;
};
//Rolling my own test framework here
$test('add_2 Should return a sum of its two arguments',
$assert_equals(3, $add_2(1, 3)));
Where to go from here
Learn JavaScript
- Eloquent JavaScript: http://eloquentjavascript.net/
- JavaScript: The Good Parts
- Professor Frisby's Mostly Adequate Guide To Functional Programming: https://drboolean.gitbooks.io/mostly-adequate-guide/content/
Learn Haskell
- Learn You A Haskell For Great Good: http://learnyouahaskell.com/
https://github.com
/NerdcoreSteve
/php_func
@ProSteveSmith
Functional Programming With PHP
By Steve Smith
Functional Programming With PHP
- 1,061