Jak z funkcí implementovat objektový systém

Když David Grudl psal článek o dependency injection, jako správný OOP programátor všude používal objekty – primitivní jednoúčelové objekty, které dělaly jednu jedinou věc a které vypadaly dost jako obyčejné funkce. Líné hodnoty řešil třídami, které by se daly nahradit jednou maličkou funkcí. Tam někde mě napadlo: „Když se dá tohle všechno nahradit funkcemi, kam až by se dalo zajít jenom s funkcemi? Bylo by možné jenom z nich implementovat celý objektový systém?“


Okamžitě se mi v paměti vybavily dva články: jeden říkal, že naprogramovat vlastní objektový systém v Lispu je naprosto triviální záležitost a druhý zmiňoval, že nejjednodušší implementace Lispu nepotřebuje ani spojové seznamy, protože ty se dají postavit z funkcí a několika speciálních forem (bohužel v tomhle případě nemůžu najít zdroj).

Vytyčil jsem si tedy jasný cíl: vezmu podmnožinu PHPčka, která bude obsahovat jenom skalární hodnoty a funkce (tedy žádná pole, žádné objekty, ani jiné kompozitní typy) a na těchto skromných základech postavím objektový systém, který bude mít otevřenou rekurzi/virtuální metody.


Když nemám nic, musím začínat od naprostých základů.

Jako první jsem musel vytvořit základní složenou datovou strukturu – funkcionální spojový seznam. Ale jak ho napsat, když nemám k dispozici žádné nativní datové struktury? Musím použít funkce a uzávěry.

Funkcionální seznam, tolik typický pro Lisp, je velice jednoduchá datová struktura. Je tvořená zřetězenými buňkami Cons, které obsahují nějakou hodnotu a ukazatel na další prvek, tím může být další Cons nebo na speciální buňka Nil ukončující seznam (nenese hodnotu ani ukazatel na další prvek).

 Cons            Cons
╭─────┬─────╮   ╭─────┬─────╮  ╭─────╮
│  *  │  * ────>│  *  │  * ───>│ Nil │
╰──|──┴─────╯   ╰──|──┴─────╯  ╰─────╯
    ∇               ∇
╭─────╮         ╭─────╮
│  x  │         │  y  │
╰─────╯         ╰─────╯

Takto sestavený seznam má zajímavé vlastnosti: jde o perzistentní datovou strukturu. Když chci na začátek seznamu přidat jeden element, vytvořím jenom novou Cons buňku (head), ukazující na existující seznam (tail), který zůstal nezměněn – oba seznamy sdílejí data. Můžu mít mnoho seznamů, které mají společné různé části. A protože jde obvykle o neměnnou datovou strukturu, je sdílení dat zcela bezpečné. Kdybych skutečně měnil data v seznamech, hrozilo by, že změním nejenom instanci, které mě zajímá, ale všechny které nějak sdílí strukturu.

Jednotlivé buňky seznamu musím vytvořit z funkcí. Nil bude reprezentován jednou sdílenou instancí funkce. To proto, abychom mohli porovnávat jeho identitu operátorem ===. Funkce samotná se nikdy nevykoná, slouží jenom jako zástupný objekt, který se liší od PHP hodnoty null. Pak vytvořím konstruktor nil(), který jednoduše vrátí instanci této funkce.

$__nilFunction = function() { return null; };
function nil() {
  global $__nilFunction;
  return $__nilFunction;
}

Teď přijde ta zajímavá část: Cons. Jak jsem už psal, Cons má dvě části: head a tail. Tato struktura se dá implementovat pomocí closure:

function cons($head, $tail) {
  return function($what) use($head, $tail) {
    return ($what[0] === 'h') ? $head : $tail;
  };
}

Konstruktor cons($head, $tail) vrátí funkci, která je uzávěrou nad parametry $head a $tail. Tak jsem dosáhl toho, že jsem zabalil dvě věci jednoho balíku. Teď jak je vybalit? Jednoduše: funkce vytvořená konstruktorem cons má jeden argument, který rozhodne, jestli vrátí $head nebo $tail. Takhle se dá zkonstruovat kompozitní struktura z „ničeho“.

Potom můžu vytvořit extraktory head() a tail() a případně pomocnou funkci makeList() pro snadnější konstrukci seznamu (tady trochu podvádím, protože používám PHP pole, ale je to jenom helper funkce, bez které bych se obešel).

function head($l) { return $l === nil() ? null  : $l('head'); }
function tail($l) { return $l === nil() ? nil() : $l('tail'); }
function makeList() {
  return array_reduce(array_reverse(func_get_args()), function ($res, $arg) { return cons($arg, $res); }, nil());
}

To je pěkné, můžete namítat, ale k čemu je nám to dobré? Ač se to nemusí zdát, k objektovému systému mám překvapivě blízko.

Ještě potřebuji metodu concat(), která spojí dva seznamy (implementoval jsem rekurzivní concat, který pracuje v lineárním čase, ale pracuje na zásobníku) a find(), která ze seznamu vrátí první prvek vyhovující predikátu.

function concat($prefix, $suffix) {
  if ($suffix === nil())       return $prefix;
  else if ($prefix === nil())  return $suffix;
  else {
    $x = function ($l) use(&$x, $suffix) {
      if ($l !== nil()) {
        return cons(head($l), $x(tail($l)));
      } else {
        return $suffix;
      }
    };
    return $x($prefix);
  }
}

function find($l, $f) {
  if ($l === nil()) return null;
  else {
    $h = head($l);
    $t = tail($l);
    if ($f($h)) return $h;
    else        return find($t, $f);
  }
}

A teď to nejdůležitější: Co je vlastně objekt? Podle smalltalkovské tradice je to entita, která odpovídá na zprávy. Tedy něco jako kolekce metod. Nebo seznam metod. Asi už tušíte odkud vítr vane.

Objekt budu reprezentovat jako seznam metod, kde každá metoda bude tvořená párem (název, tělo metody). Tělo metody je pak funkce, která jako první argument přijímá $self reprezentující instanci objektu nad kterým metodu volám.

Teď už jenom potřebuji funkci call(), která se pokusí najít metodu s daným jménem a pak ji zavolá:

function call($object, $method) {
  $m = find($object, function ($x) use($method) { return head($x) === $method; });
  $m = tail($m);
  return $m($object);
}

Dědičnost vyřeším delegací. Jde o nejjednodušší přístup. Prototypová dědičnost by nebyla o moc složitější, ale delegace je v tomto systému naprosto triviální. Delegace je jednoduchý concat. Nové metody do potomka přidám jednoduše tak, že je přilepím na začátek seznamu metod. Takto přidané metody budou nalezeny dřív než ty předchozí.

function deleg($childMethods, $parentMethods) {
  return concat($childMethods, $parentMethods);
}

$child = deleg($childMethods, $parentMethods);

A to je všechno. Právě jsem naprogramoval objektový systém s otevřenou rekurzí.

Virtuální metody fungují díky tomu, že metody jsou hledány od začátku seznamu a tedy metody potomků jsou vždycky nalezeny a zavolány jako první.

Tyto objekty jsou v principu neměnné. Když chci změnit nějakou metodu (a properties jsou jenom bezparametrické metody) jednoduše změněnou implementaci připojím na začátek seznamu metod, stará metoda se neodstraní, ale jelikož je v seznamu dál, nikdy nebude v modifikovaném objektu nalezena a zavolána.

// definice předka
$parent = makeList(
  cons("x", function ($self) { return 'parent.x'; }),
  cons("y", function ($self) { return 'parent.y -> ' . call($self, 'x'); })
);

// metody potomka
$childMethods = makeList(
  cons("x", function ($self) { return 'child.x'; })
);

// definice potomka
$child = deleg($childMethods, $parent);


var_dump( call($parent, 'x') === 'parent.x' );
var_dump( call($parent, 'y') === 'parent.y -> parent.x');

var_dump( call($child,  'x') === 'child.x' );
// metoda definovaná v rodiči volá metodu předefinovanou v potomkovi
var_dump( call($child,  'y') === 'parent.y -> child.x');

Properties a konstruktory můžu přidat velice snadno.

function property($val) { return function () use($val) { return $val; }; }

// $makePerson je konstruktor
$makePerson = function ($firstname, $secondname) {
  return makeList(
    cons('firstname',  property($firstname)),
    cons('secondname', property($secondname)),
    cons('fullname', function ($self) { return 'full name: ' . call($self, 'firstname') . call($self, 'secondname'); })
  );
};

$person1 = $makePerson('Karel', 'Kalandra');
$person2 = deleg(makeList(cons('secondname', property('Čížex'))), $person);

var_dump(call($person1, 'fullname') === "full name: Karel Kalandra");
var_dump(call($person2, 'fullname') === "full name: Karel Čížex");

Samozřejmě jde o značně primitivní a neefektivní implementaci objektového systému (hledání metod má lineární složitost, opakované mutace/delegace mohou neúměrně navýšit velikost objektu, metody nepřijímají argumenty), ale je funkční a všechny chybějící funkce můžu poměrně snadno dodělat.

Kompletní zdrojový je k dispozici na githubu.

Flattr this!

This entry was posted in Funkcionální programování, OOP, PHP. Bookmark the permalink.

11 Responses to Jak z funkcí implementovat objektový systém

  1. v6ak says:

    Zajímavé. BTW, u $__nilFunction; bych použil static, zbavíš se tak jedné globální proměnné.

  2. Jiří Knesl says:

    Zajímavý článek. V implementaci OOP si zašel ještě o kousek „níž“, tedy víc k jednoduchosti.

    Já jsem před pár měsíci taky implementoval minimalistické OOP v PHP. Použití je extrémně jednoduché.

    Tady je projekt: https://bitbucket.org/…nesl/php-oop

    Tady je zdroják: https://bitbucket.org/…d/Object.php?…

    • To je pěkné, ale podstatně se liší naše výchozí podmínky. Já jsem začínal bez kompozitních typů/jenom s funkcemi/uzá­věrami, ty bereš asoc. pole jako daná. Mezi tím je gigantický rozdíl.

  3. OO v procedurálním jazyce není podle mě nová věc. V linuxu se s úspěchem používá knihovna GObject tuším.

  4. I když to nemění nic na tom, že v PHP je to machrovina :)

Leave a Reply

Your email address will not be published. Required fields are marked *