funkcionálně.cz

Přední český blog o funkcionálním programování, kde se o funkcionálním programování nepíše
««« »»»

Jak z funkcí implementovat objektový systém

22. 3. 2013 — k47

Když David Grudl psal článek o de­pen­dency in­jection, jako správný OOP pro­gra­má­tor všude po­u­ží­val ob­jekty – pri­mi­tivní jed­no­úče­lové ob­jekty, které dělaly jednu je­di­nou věc a které vy­pa­daly dost jako oby­čejné funkce. Líné hod­noty řešil tří­dami, které by se daly na­hra­dit jednou ma­lič­kou funkcí. Tam někde mě na­padlo: „Když se dá tohle všechno na­hra­dit funk­cemi, kam až by se dalo zajít jenom s funk­cemi? Bylo by možné jenom z nich im­ple­men­to­vat celý ob­jek­tový systém?“


Oka­mžitě se mi v paměti vy­ba­vily dva články: jeden říkal, že na­pro­gra­mo­vat vlastní ob­jek­tový systém v Lispu je na­prosto tri­vi­ální zá­le­ži­tost a druhý zmi­ňo­val, že nej­jed­no­dušší im­ple­men­tace Lispu ne­po­tře­buje ani spo­jové se­znamy, pro­tože ty se dají po­sta­vit z funkcí a ně­ko­lika spe­ci­ál­ních forem (bo­hu­žel v tomhle pří­padě nemůžu najít zdroj).

Vy­ty­čil jsem si tedy jasný cíl: vezmu pod­mno­žinu PHPčka, která bude ob­sa­ho­vat jenom ska­lární hod­noty a funkce (tedy žádná pole, žádné ob­jekty, ani jiné kom­po­zitní typy) a na těchto skrom­ných zá­kla­dech po­sta­vím ob­jek­tový systém, který bude mít ote­vře­nou re­kurzi/vir­tu­ální metody.


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

Jako první jsem musel vy­tvo­řit zá­kladní slo­že­nou da­to­vou struk­turu – funk­ci­o­nální spo­jový seznam. Ale jak ho napsat, když nemám k dis­po­zici žádné na­tivní datové struk­tury? Musím použít funkce a uzá­věry.

Funk­ci­o­nální seznam, tolik ty­pický pro Lisp, je velice jed­no­du­chá datová struk­tura. Je tvo­řená zře­tě­ze­nými buň­kami Cons, které ob­sa­hují ně­ja­kou hod­notu a uka­za­tel na další prvek, tím může být další Cons nebo na spe­ci­ální buňka Nil ukon­ču­jící seznam (nenese hod­notu ani uka­za­tel na další prvek).

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

Takto se­sta­vený seznam má za­jí­mavé vlast­nosti: jde o per­zis­tentní da­to­vou struk­turu. Když chci na za­čá­tek se­znamu přidat jeden ele­ment, vy­tvo­řím jenom novou Cons buňku (head), uka­zu­jící na exis­tu­jící seznam (tail), který zůstal ne­změ­něn – oba se­znamy sdí­lejí data. Můžu mít mnoho se­znamů, které mají spo­lečné různé části. A pro­tože jde ob­vykle o ne­měn­nou da­to­vou struk­turu, je sdí­lení dat zcela bez­pečné. Kdy­bych sku­tečně měnil data v se­zna­mech, hro­zilo by, že změním neje­nom in­stanci, které mě zajímá, ale všechny které nějak sdílí struk­turu.

Jed­not­livé buňky se­znamu musím vy­tvo­řit z funkcí. Nil bude re­pre­zen­to­ván jednou sdí­le­nou in­stancí funkce. To proto, abychom mohli po­rov­ná­vat jeho identitu ope­rá­to­rem ===. Funkce sa­motná se nikdy ne­vy­koná, slouží jenom jako zá­stupný objekt, který se liší od PHP hod­noty null. Pak vy­tvo­řím kon­struk­tor nil(), který jed­no­duše vrátí in­stanci této funkce.

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

Teď přijde ta za­jí­mavá část: Cons. Jak jsem už psal, Cons má dvě části: headtail. Tato struk­tura se dá im­ple­men­to­vat pomocí clo­sure:

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

Kon­struk­tor cons($head, $tail) vrátí funkci, která je uzá­vě­rou nad pa­ra­me­try $head$tail. Tak jsem dosáhl toho, že jsem za­ba­lil dvě věci jed­noho balíku. Teď jak je vy­ba­lit? Jed­no­duše: funkce vy­tvo­řená kon­struk­to­rem cons má jeden ar­gu­ment, který roz­hodne, jestli vrátí $head nebo $tail. Takhle se dá zkon­stru­o­vat kom­po­zitní struk­tura z „ničeho“.

Potom můžu vy­tvo­řit ex­trak­tory head()tail() a pří­padně po­moc­nou funkci makeList() pro snad­nější kon­strukci se­znamu (tady trochu pod­vá­dím, pro­tože po­u­ží­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 na­mí­tat, ale k čemu je nám to dobré? Ač se to nemusí zdát, k ob­jek­to­vému sys­tému mám pře­kva­pivě blízko.

Ještě po­tře­buji metodu concat(), která spojí dva se­znamy (im­ple­men­to­val jsem re­kur­zivní concat, který pra­cuje v li­ne­ár­ním čase, ale pra­cuje na zá­sob­níku) a find(), která ze se­znamu vrátí první prvek vy­ho­vu­jící pre­di­ká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 nej­dů­le­ži­tější: Co je vlastně objekt? Podle small­tal­kov­ské tra­dice je to entita, která od­po­vídá na zprávy. Tedy něco jako ko­lekce metod. Nebo seznam metod. Asi už tušíte odkud vítr vane.

Objekt budu re­pre­zen­to­vat 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í ar­gu­ment při­jímá $self re­pre­zen­tu­jící in­stanci ob­jektu nad kterým metodu volám.

Teď už jenom po­tře­buji 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 de­le­gací. Jde o nej­jed­no­dušší pří­stup. Pro­to­ty­pová dě­dič­nost by nebyla o moc slo­ži­tější, ale de­le­gace je v tomto sys­tému na­prosto tri­vi­ální. De­le­gace je jed­no­du­chý concat. Nové metody do po­tomka přidám jed­no­duše tak, že je při­le­pím na za­čá­tek se­znamu metod. Takto při­dané metody budou na­le­zeny dřív než ty před­chozí.

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

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

A to je všechno. Právě jsem na­pro­gra­mo­val ob­jek­tový systém s ote­vře­nou re­kurzí.

Vir­tu­ální metody fun­gují díky tomu, že metody jsou hle­dány od za­čátku se­znamu a tedy metody po­tomků jsou vždycky na­le­zeny a za­vo­lány jako první.

Tyto ob­jekty jsou v prin­cipu ne­měnné. Když chci změnit ně­ja­kou metodu (a pro­per­ties jsou jenom bez­pa­ra­me­t­rické metody) jed­no­duše změ­ně­nou im­ple­men­taci při­po­jím na za­čá­tek se­znamu metod, stará metoda se ne­od­straní, ale je­li­kož je v se­znamu dál, nikdy nebude v mo­di­fi­ko­va­ném ob­jektu na­le­zena a za­vo­lá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');

Pro­per­ties a kon­struk­tory 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('Hrozný'))), $person);

var_dump(call($person1, 'fullname') === "full name: Karel Kalandra");
var_dump(call($person2, 'fullname') === "full name: Karel Hrozný");

Sa­mo­zřejmě jde o značně pri­mi­tivní a ne­e­fek­tivní im­ple­men­taci ob­jek­to­vého sys­tému (hle­dání metod má li­ne­ární slo­ži­tost, opa­ko­vané mutace/de­le­gace mohou ne­ú­měrně na­vý­šit ve­li­kost ob­jektu, metody ne­při­jí­mají ar­gu­menty), ale je funkční a všechny chy­bě­jící funkce můžu po­měrně snadno do­dě­lat.

Kom­pletní zdro­jový je k dis­po­zici na gi­thubu.

@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články