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

Google

Learn JavaScript

Learn Haskell

https://github.com

/NerdcoreSteve

/php_func

@ProSteveSmith

Functional Programming With PHP

By Steve Smith

Functional Programming With PHP

  • 1,061