Jeden můj kamarád dostal při pohovoru (nikoliv do Facebooku) zajímavou otázku: „Naprogramuj JavaScriptovou funkci setTimeout
v PHP.“ Jak jsem to slyšel, začala mi hlava šrotovat, vymýšlet řešení a narážet na první komplikace ještě dřív, než jsem vzal do ruky tužku nebo klávesnici. A i když jsem si nejdřív říkal, že je to úloha zaměřená hlavně na technické detaily implementace místo na algoritmizaci (o což by mělo jít víc), tak se v ní algoritmus přeci jen použije. Tím pádem se dá rozebrat jeho časová složitost, která je při přímočaré implementaci špatná, ale dá se vylepšit. Prostě mi z toho nakonec vychází perfektní úloha k pohovoru.
Pokud vás úloha taky zaujala, zkuste si ji nejprve vyřešit sami:
Nevíte si s něčím rady? Nejste si jisti, jestli jste na něco nezapomněli? Podívejte se na nápovědu. Časem se dostaneme i k mému řešení.
setTimeout(f, 0)
, tak se funkce nespustí hned, ale teprve až když doběhne všechno ostatní. Není tedy potřeba lámat si hlavu s nějakým asynchronním plánováním a stačí všechno dávat do fronty a tu na konci spustit.processSetTimeout
? Srabi! register_shutdown_function neznáte?Pokud jste nápovědu zohlednili, podívejte se na příklad, který by váš skript měl umět zpracovat.
<?php Window::setTimeout(function () { echo "Wo"; Window::setTimeout(function () { echo "d!\n"; }, 1000); }, 1000); Window::setTimeout(function () { echo "rl"; }, 1500); echo "Hello "; ?>
Příklad by měl pochopitelně vypsat Hello World!. Mazali jste se místo statických metod s normálními? A není náhodou čas tak trochu globální veličina?
Pokud vám všechno funguje, podívejte se na mé první řešení a porovnejte ho se svým.
<?php class Window { static private $timeouts; static function setTimeout(callable $function, $ms) { if (self::$timeouts === null) { register_shutdown_function('Window::shutdown'); } self::$timeouts[] = [ microtime(true) + $ms / 1e3, $function ]; } /** @internal */ static function shutdown() { while (self::$timeouts) { $val = min(self::$timeouts); $key = array_search($val, self::$timeouts); unset(self::$timeouts[$key]); list($time, $function) = $val; $now = microtime(true); if ($time > $now) { usleep(($time - $now) * 1e6); } $function(); } } } ?>
V řešení jsou použity dvě frajeřinky z PHP 5.4 – typehint callable a zkrácená syntaxe polí. Samozřejmě by to fungovalo i bez nich, ale měl jsem nutkavou potřebu ukázat, že jsem in.
Na řešení je zajímavá jiná věc: Samozřejmě mě nejprve napadlo v metodě shutdown
prvky setřídit podle cílového času a projít je cyklem foreach. To by ale narazilo v momentě, když by někdo v jednom timeoutu zaregistroval druhý. Takže se timeout vybírá funkcí min. Ta funguje stejně jako porovnávací operátory i s poli podle dokumentovaného algoritmu.
Taky mě nejprve napadlo čas ukládat do klíče ($timeouts[$time][] = $function
), ale to narazí na to, že PHP v klíčích pole dovoluje používat pouze celá čísla nebo řetězce. A vynásobení 1e6 by zase přesáhlo maximální velikost celého čísla.
Jaká je časová složitost algoritmu? Nejprve si ji sami odvoďte. Rozmyslete si, jestli by se nedala vylepšit. Pak porovnejte.
Funkce min musí prohlédnout všechny prvky pole, takže stojí O(N). Zavolá se O(N)–krát, celkem tedy algoritmus běží v čase O(N²). Zlepšit by se to dalo tak, že bychom prvky do pole průběžně zatřiďovali v čase O(log N) a vždy jen vybrali ten nejmenší. Celkem by se čas tedy zkrátil na O(N * log N). V PHP ale nevím o funkci, která by nám v setříděném poli rychle našla nejbližší prvek a vložila vedle něj další. A pokud bychom si to psali sami, tak je otázka, co by se složitostí udělalo volání funkce array_splice, kterou bychom nejspíš použili.
Teď taková objektová odbočka: Jak se zbavit příznaku @internal
a udělat metodu shutdown
zvenčí neviditelnou? Máte? Já taky.
@internal
Metodu shutdown
úplně zrušíme a při registraci zavoláme:
<?php register_shutdown_function(function ($timeouts) { // ... původní obsah metody shutdown }, self::$timeouts); ?>
To ale nestačí – pole se v PHP předávají kopií, takže by se ztratila vazba na vlastnost třídy. A funkce register_shutdown_function nepodporuje předávání parametrů referencí. Vyřešit by se to dalo tím, že bychom ze self::$timeouts
udělali objekt. Psát se mi to nechce. Alternativou je nastavit $timeouts = &self::$timeouts
a poslat to callbacku přes use (&$timeouts)
.
Teď je čas to „mírně“ upravit: Co kdybychom se nespokojili s tím, že další funkci stačí zavolat, až když doběhne předchozí kód a místo toho bychom ji chtěli spustit co nejdřív, jakmile bude její čas? Prostě takové přerušení. Následující kód by tedy měl opět vypsat Hello World!:
<?php Window::setTimeout(function () { echo " World"; }, 550); foreach (preg_split('~~', "Hello!\n") as $ch) { echo $ch; usleep(100000); } ?>
Troufnete si? Nejspíš to nebude jen drobná změna, ale fundamentálně odlišné řešení. Zkuste si ho napsat:
Možná si říkáte, že něco takového v PHP nejde. Ale jde.
declare
Řešení spočívá v použití neobvyklé konstrukce declare(ticks = 1). Kamarád mi ani nevěřil, že v PHP něco takového existuje. Ani já jsem to nikdy smysluplně nepoužil, ale tohle je ideální příležitost:
<?php declare(ticks = 1); class Window { static private $timeouts; static function setTimeout(callable $function, $ms) { if (self::$timeouts === null) { register_tick_function('Window::tick'); } self::$timeouts[] = [ microtime(true) + $ms / 1e3, $function ]; } static function tick() { if (!self::$timeouts) { return; } $val = min(self::$timeouts); $key = array_search($val, self::$timeouts); list($time, $function) = $val; if (microtime(true) > $time) { unset(self::$timeouts[$key]); $function(); } } } ?>
Normální program by se tím příšerně zpomalil, protože volání microtime je dost drahé. Celkové řešení bychom navíc museli sestavit kombinací prvního a druhého, já jsem v ukázce záměrně nechal jen to zajímavé. Ale jako řešení u pohovoru by to myslím stačilo.
Příklad se mi natolik líbil, že ho asi začnu o pohovorů dávat sám.
Diskuse je zrušena z důvodu spamu.