PHP curry — jakubův notes – programování a vejšplechty

Píšu o:

U funkcionálních jazyků je velice častý tzv. „currying“, neboli možnost vytvořit novou funkci dosazením nějakých výchozích argumentů do již existující funkce. Jak na to v PHP?

Příklad z Haskellu:

add a b = a + b
add3 = add 3

Pokud znáte funkcionální jazyky, klidně následující dva odstavce přeskočte.

Funkcionální jazyky zakládají výpočet na zjednodušování – zjednodušuje se tak dlouho, až je konečný výraz dále nezjednodušitelný. add a b = a + b tedy říká, že pokud bychom měli výraz add 3 5, můžeme ho zjednodušit na 3 + 5. Neboli všechen výskyt proměnné a je v tomto případě nahrazen číslem 3 a proměnná b je zase nahrazena číslem 5.

Když se podíváte na deklaraci add3, tak to je to samé. Parametr 3 je aplikován na funkci add, takže všechen výskyt a, tj. prvního parametru, je nahrazen číslem 3. A tak by se tedy add3 dalo přepsat jako add3 b = 3 + b – parametr a byl nahrazen číslem 3, parametr b zůstal.

V PHP takovéto hezké možnosti nemáme. Ani rozšíření syntaxe by tomu nepomohlo, protože někdy takovéto funkce chceme třeba předat jako callback a nelze nijak donutit, aby vnitřní funkce volali naši upravenou syntax. Ale jde to udělat jinak. Jsou vlastně dva způsoby:

  1. closures a

  2. funkční objekt

Closures

Nejdříve, jak by vypadalo add v PHP:

function add($a, $b) { return $a + $b; }

A teď je třebas potřeba tuto funkci s parametrem prvním parametrem 3 aplikovat na pole pomocí array_map(). V Haskellu by to opět šlo jednoduše: map (add 3) [1,2,3]. V PHP mohu pomocí closures:

array_map(function ($b) { return add(3, $b); }, array(1, 2, 3));

Problém je v tom, že closures pracují až od PHP 5.3 a není to až tak elegantní.

Funkční objekt

Jak by vypadalo volání array_map() s funkčním objektem (zdrojový kód funkčního objektu na konci článku)?

array_map(fn('add', array(3, fn::ph()), array(1, 2, 3));

Podle mě hezčí a je to i kratší. Jak se asi dá vytušit, prvním argumentem fn() je callback a druhým seznam parametrů. Co je to fn::ph()? To je placeholder, neboli parametr, který drží místo pro parametr, který se dosadí, až při volání funkčního objektu. fn() je sám o sobě helper, který vytváří instanci třídy fn. Ta zastřešuje funkční objekt. (Od PHP 5.3 už nebude tohoto helperu zapotřebí díky magické metodě __invoke().)

Ten placeholder by tam ani být nemusel, ale může se v některých případech hodit:

function div($a, $b) { return $a / $b; }
$div2 = fn('div', array(fn::ph(), 2));

Kdyby nebylo placeholderu, jen těžko by funkční objekt věděl, kam má dosadit přijaté parametry. Dalším vylepšením by mohlo být, že by placeholder přijímal index, na kterém se bude parametr při volání funkčního objektu nacházet. Ale pro to jsem už využití nenašel, takže to implementovat nebudu – YAGNI.

Taky by se mohlo vytváření funkčního objektu mohlo se využitím func_get_args(), func_get_arg() a func_num_args() ještě zjednodušit na:

fn('add', 3, fn::ph())

Ale pak by bylo problém s argumenty předávanými referencí, protože na:

fn('foo', &$bar);

by PHP vyhodilo krásný warning Call-time pass-by-reference has been deprecated. Při použití:

fn('foo', array(&$bar));

je se uklidní a nic nevyhazuje. Ale pokud by absence předávání parametru jako reference nevadila, je to opět další způsob, jak věc udělat ještě elegantnější.

Zdrojový kód

<?php
/**
 * Wraps callback with some default params
 */
class fn
{
    /**
     * @var callback Wrapped callback
     */
    private $callback;

    /**
     * @var array Wrapped callback params
     */
    private $params;

    /**
     * @var mixed Instance params placeholder
     */
    private $placeholder;

    /**
     * Constructor
     * @param callback to wrap
     * @param array
     * @param mixed instance params placeholder
     */
    public function __construct($callback = '', array $params = array())
    {
        $this->callback = $callback;
        $this->params = $params;
        if (func_num_args() === 3) $this->placeholder = func_get_arg(2);
        else $this->placeholder = self::ph(); 
            // if not given, use default placeholder
    }

    /**
     * Calls wrapped callback
     * @return mixed
     */
    public function __invoke()
    {
        $args = func_get_args();
        $params = array();
        foreach ($this->params as &$_)
            if ($_ === $this->placeholder) $params[] =& array_shift($args);
            else $params[] =& $_;
        return call_user_func_array($this->callback, $params);
    }

    /**
     * Default placeholder getter/setter
     * @return mixed
     */
    public static function ph()
    {
        static $placeholder;
        if (func_num_args() === 0) return $placeholder;
        else return $placeholder = func_get_arg(0);
    }
}

/**
 * Create new instance of fn a return fn::__invoke() callback
 * @param callback to wrap
 * @param array
 * @param mixed instance params placeholder
 * @return callback
 */
function fn($callback = '', array $params = array())
{
    if (func_num_args() < 3) $_ = new fn($callback, $params);
    else $_ = new fn($callback, $params, func_get_arg(2));
    return array($_, '__invoke');
}

Doposud žádný komentář

Přidat komentář