Striktní a líné jazyky
Četl jsem jeden článek, který tvrdil, že operátory nejsou (vždy) funkce. To je pravda a není k tomu třeba dodávat nic víc.
Stojí to ale za pozornost jen v případě striktních jazycích typu C a Java, kde
jsou argumenty funkce vyhodnoceny vždy před jejím zavoláním (applicative
order/call by value). V takovém prostředí není jednoduše možné napsat
vlastní funkci OR nebo AND, která se chová stejně jako vestavěné operátory ||
nebo &&
. Ty v klasické sortě jazyků podobných C vyhodnotí druhý argument
jen pokud je to nezbytně nutné. Toho bez funkcí vyššího řádu (jak FR podotknul)
není možné dosáhnout. Proto v nich operátory zaujímají speciální místo.
Mě to koplo nedávno, když jsem psal interpret jednoduchého lipovského
jazyka1 , kde odloženého vyhodnocování bylo nezbytné k tomu, aby následující
kód neskonšil NullPointerException
.
(if (and (nil? x) (.nonEmpty x)) (something))
V líných jazycích nebo těch, které podporují call-by-name nebo call-by-need, operátory v principu nemají speciální postavení2 a jsou to jen další metody.
Ve Scale můžu napsat vlastní funkci OR takto:
def OR(a: Boolean, b: => Boolean): Boolean = if (a) true else b
Typ => Boolean
označuje call-by-name boolean. b
není vyhodnoceno při
zavolání funkce, namísto toho bude vyhodnoceno na každém místě, kde je v těle funkce použito. Kompilátor call-by-name parametr zabalí do anonymní
funkce, která je pak opakovaně volána.
Nejde ale o plně líné neboli call-by-need vyhodnocování jako například v Haskellu. V něm je nevyhodnocený výraz reprezentován strukturou běžně nazývanou thunk, která ukazuje na kód. Když je přinucena se vyhodnotit (například pattern matchingem), je odkazovaný program vykonán a thunk je atomicky zaměněn na výsledek. Líné jazyky mají tu vlastnost, že nevyhodnocené nemusí být jen argumenty funkce, ale celé datové struktury. Můžu mít masivní strom, ale když jsem četl jen z jedné cesty k listům, všechny nenavštívené odbočky zůstanou jako nevyhodnocené thunky.
Call-by-need je tedy call-by-name doplněné s memoizací.
Ve Scale je to možné emulovat takto:
def f(_a: => X) = { lazy val a = _a ??? }
Když někde použiji proměnnou a
dojde k jejímu vyhodnocení jen jednou a výsledná hodnota se bude recyklovat při každém dalším použití.
Poznámky:
- Dijkstra upozornil, že short-circuit operátory porušují pravidla distributivity a měli bychom se jim vyhýbat, neboť ztěžují porozumění programu.
- Jmenoval se Lispy a byl zamýšlený jako skriptovací nástroj pro asciiblog. Psalo se v něm příjemně, ale nakonec jsem ho nahradil za javascript vestavěný do Javy.
- Kompilátor o nich ví a proto bych si dovolil odhadovat, že pro ně vygeneruje lepší kód.