Změna chování {default} v Latte – sjednocení s ??= operátorem

David Grudl
Nette Core | 8218
+
0
-

Rád bych otevřel diskusi ohledně možné změny chování značky {default} v Latte pro budoucí verze (např. 3.1).

  • Značka {default $var = 123} vytvoří proměnnou, pokud neexistuje.
  • Pokud $var = null, značka {default} ponechá hodnotu null.
  • Toto chování vzniklo dávno před zavedením PHP operátoru ??=.
  • PHP operátor ??= (nullsafe assignment operator) přepíše hodnotu null.

Zvažuji, zda by nebylo vhodné sjednotit chování {default} s PHP operátorem ??=. To by znamenalo, že by {default} přepisovalo i null hodnoty.

Myslíte, že by tato změna byla přínosná? Bylo by vhodné udělat toto chování konfigurovatelné? Pokud ano, jak by mohl vypadat způsob konfigurace?

Díky za názory a podněty.

mystik
Člen | 308
+
+4
-

Pokud explicitne predam do parameteu null cekal bych ze zustane zachovane null. Stejne jako u parametru volani metod.

Dovedl bych si ale predstavit ze by bylo uzitecne mit jednoduchy zapis pro doplneni default null hodnot.

Co treba takhke: {default $var ??= 123}

David Grudl
Nette Core | 8218
+
+1
-

Takový zápis existuje. {do $var ??= 123}

mystik
Člen | 308
+
+6
-

Pak bych to nechal jak to je.

Felix
Nette Core | 1196
+
0
-

Ja bych byl pro sjednoceni s PHP.

mystik
Člen | 308
+
0
-

Měnilo se nějak chování {default} v nové verzi Latte?

Začaly nám totiž padat testy u efabrica/phpstan-latte při analýze šablon s tímto tagem.

Šablona obsahuje:

{default $someVariableWithDefault = 'default value'}

Proměnná $someVariableWithDefault není definovaná (což analyzátor ví).

Dřív to PHPStan vyhodnotil že tak, že typ proměnné je 'default value', ale teď to dává 'default value'|null

Je to záměrná změna chování nebo chyba v analýze?

Vygenerovaný kód v šabloně vypadá teď takto:

$someVariableWithDefault ??= \array_key_exists('someVariableWithDefault', \get_defined_vars()) ? \null : 'default value';

Z toho mi vychází že pokud proměnná není definovaná mělo by se přiřadit 'default value', ale PHPStan možná nedává tu „magii“ s array_key_exists a get_defined_vars.

Editoval mystik (31. 10. 13:26)

David Grudl
Nette Core | 8218
+
0
-

Nic se nemenilo

mystik
Člen | 308
+
0
-

Vypadá to na limitaci PHPStanu. buď to zkusíme nějak dořešit v PHPStanu nebo budem dělat nějaký preprocessing

David Grudl
Nette Core | 8218
+
0
-

A co by mělo být správné chování? Typ proměnné můžu být vlastně cokoliv, včetně null.

hrach
Člen | 1838
+
0
-

PHP zavedlo null jako plnohodnotny „typ“, tzn. povazovat null hodnotu za „undefined variable“, pro ktery potrebuji default, mi prijde neprakticke a „mirici“ opacnym smerem.

m.brecher
Generous Backer | 863
+
0
-

V diskusi zazněly názory, že je trend používat v PHP null nikoliv ve smyslu ‚undefined‘, ale spíše jako plnohodnotnou ‚default‘ hodnotu:

@mystik

Pokud explicitne predam do parameteu null cekal bych ze zustane zachovane null .....

@hrach

PHP zavedlo null jako plnohodnotny „typ“, tzn. povazovat null hodnotu za „undefined variable“, pro ktery potrebuji default, mi prijde neprakticke a „mirici“ opacnym smerem.

S tímto názorem bych nesouhlasil. Napříč jazyky včetně PHP je primárním významem null ‚undefined‘ a tak by se null mělo v PHP používat. Rozdíl mezi ‚undefined‘ a null by měl být jenom ten, že hodnotou null vědomě deklaruji že hodnotu neznám a nechci vyhazovat výjimku. Tag {default} ale není tak důležitý a podpora pro změnu v komunitě nulová, takže z pragmatického pohledu nemá asi cenu změnu dělat.

Editoval m.brecher (1. 11. 19:38)

mystik
Člen | 308
+
+1
-
function foo($param = 'default') {
  return $param;
}

foo(); // 'default'
foo(null); // null

Tamto se to chova u defaultu parametru funkce. IMHO by se to melo stejne chovat u defaultu parametru sablony.

m.brecher
Generous Backer | 863
+
+1
-

@mystik

Tamto se to chova u defaultu parametru funkce. IMHO by se to melo stejne chovat u defaultu parametru sablony.

Máš pravdu, je to docela silný argument proti změnám. PHP funkce se opravdu chová, jako kdyby null nemělo význam ‚undefined‘, ale jako kdyby šlo o nějakou hodnotu.

Pojďme se podívat na jiné části PHP / Nette Frameworku, jak rozlišují mezi null a ‚undefined‘:

nullsafe operátory PHP

$var;							 // undefined
bdump($var ?? 'default');        // 'default'
$var = null;
bdump($var ?? 'default');        // 'default'
$var;               // undefined
$var ??= 'default';
bdump($var);        // 'default'
$var = null;
$var ??= 'default';
bdump($var);        // 'default'
}

presentery nette, parametr var je ‚undefined‘

public function actionDefault(?string $var): void
{
    bdump($var);        // null
}
public function actionDefault(?string $var = 'default'): void
{
    bdump($var);        // 'default'
}

Nullsafe operátory PHP nerozlišují mezi null a ‚undefined‘, akce presenterů Nette předá do neexistujícího parametru hodnotu null, kterou lze přepsat defaultní hodnotou. Tyto prvky tedy mezi ‚undefined‘ a null nerozlišují.

Tag {default} byl navržen v době, kdy moderní nullsafe operátory neexistovaly. PHP se ale vyvíjí směrem k modernímu jazyku a nově přidávané prvky už respektují všeobecně uznávané normy a pravidla běžné v moderních jazycích. Nette bylo vždy o krok před PHP a snažilo se staré nepovedené koncepty nějak opravit.

PHP nemá defaultní hodnotu parametru funkce správně navrženu, ale nullsafe operátory už správně navrženy jsou. Nette by si mělo v PHP vzít inspiraci z těch moderních dobře navržených koncepcí, nikoliv z těch starých, nepovedených, které tam zůstávají kvůli BC.

Samozřejmě změny jsou nepříjemné a vyžadují přepsání určitého počtu šablon. Pokud by se nové chování dalo konfigurací vypnout, tak by se přece změny nemusely nikoho dotknout ??

hrach
Člen | 1838
+
+4
-

@mbrecher Podivej se na to spis takto:

  • nullsafe operator je neco jineho nez default hodnota a navic uz jde v latte pouzit
  • predstava, ze mi nekdo prepisuje explicitne nastaveny null me desi, ikdyz chapu, ze tu zadna detekce na explicitne nastaveny null nemuze byt;
  • default chapu jako vhodny konstrukt, kdyz je sablona pouzita v ruznych flow a nekde proste tu variable nenastavim

Obecne bych nedelal zbytecny bc breaky. Pokud potrebujeme “replacovat” null a chceme to mit jako macro, zavedl bych nove makro. {defaultIfNull $var = 123}

Kamil Valenta
Člen | 815
+
0
-

Souhlas s @hrach
Nemyslím si, že nerozlišování null a undefined je napříč jazyky. Třeba Python a JavaScript je rozlišují. A skutečně, pokud někde v argumentu uvedu null, očekávám tam null, ne cokoliv jiného, co je výchozí hodnotou. Výchozí hodnotu čekám jen v případě undefined.

Naopak bych řekl, že ta vágnost při vyhodnocování v PHP je spíš obtíž, každého asi někdy potrápila empty()…

Než ale zakládat novou variantu makra defaultIfNull, rozšířil bych podporu pro {default $var ??= 123}, jak navrhoval @Mistik
I když to jde přes makro do, připadá mi intuitivní mít k dispozici {default $var = 123} a {default $var ??= 123}, rozdíl je na první pohled jasný, člověk si nemusí pamatovat nic nového a řeší situaci vždy stejným makrem.

Editoval Kamil Valenta (5. 11. 12:49)

m.brecher
Generous Backer | 863
+
0
-

@hrach

predstava, ze mi nekdo prepisuje explicitne nastaveny null me desi

@KamilValenta

A skutečně, pokud někde v argumentu uvedu null, očekávám tam null, ne cokoliv jiného, co je výchozí hodnotou.

Explicitní nastavení hodnoty null, která se nesmí přepsat defaultní hodnotou, zatímco ‚undefined‘ se přepsat musí, signalizuje použití null s jiným významem než ‚undefined‘. Dělá se to běžně, ale vede to k méně čitelnému kódu a vznikají komplikace. Komponenta předá do šablony null s tím, že se null nesmí přepsat defaultní hodnotou. Autor šablony ale používá null stejně jako ‚undefined‘ a místo {default $num = 0} použije {do $num ??= 0}. Autor komponenty nemůže zabránit tomu, aby autor šablony nepřepsal null defaultní hodnotou. I když funkci {default} nebudeme měnit může nastat tato „děsivá“ varianta :).

Jak se „děsivé“ variantě vyhnout? Ideálně tak, že všichni budeme používat null ve významu ‚undefined‘. Jestliže vznikne situace, že přepsání null defaultní hodnotou způsobí problém, použijme místo null jinou hodnotu (0, false) a ponechejme null volné pro možnost explicitní deklarace ‚undefined‘.

Souhlasím ale s @KamilValenta, @mystik, @hrach, že ideální řešení je rozšířit syntaxi {default $var ??= 123}, a nedělat BC breaky, když to není nutné.

Editoval m.brecher (6. 11. 0:37)

David Grudl
Nette Core | 8218
+
0
-

Jsem nečekal, že taková blbost rozproudí takovou diskusi :-) Otázka spíš mířila na to, co by vám čistě prakticky víc vyhovovalo. Sám jsem se spálil a při pohledu na kód:

{default $lang = cs}
<html lang={$lang}>

…si neuvědomil, že $lang může být null. Ale holt to stačí jen vědět a jede se dál.

Kamil Valenta
Člen | 815
+
0
-

m.brecher napsal(a):

vede to k méně čitelnému kódu

To je subjektivní pohled na věc. Mystik uvedl příklad s foo(), který je naopak velmi čitelný a jiné chování by bylo matoucí.

Autor šablony ale používá null stejně jako ‚undefined‘ a místo {default $num = 0} použije {do $num ??= 0}. Autor komponenty nemůže zabránit tomu, aby autor šablony nepřepsal null defaultní hodnotou.

Autor šablony ví proč nerozlišuje (nebo naopak rozlišuje) null od undefined. A autorovi komponenty to může být jedno, ten předal co chtěl a šablona se s tím vypořádá podle svého.
Jedna šablona může být sdílena více komponentami, ne každá komponenta do ní pošle stejné atributy (undefined), ale když už je pošle, mohou být prázdné (null).

Jestliže vznikne situace, že přepsání null defaultní hodnotou způsobí problém, použijme místo null jinou hodnotu (0, false) a ponechejme null volné pro možnost explicitní deklarace ‚undefined‘.

Rozumím tomu dobře, že zavrhuješ používání null ve významu null a undefined ve významu undefined – místo toho chceš používat null a undefined ve významu undefined a 0+false ve významu null? Opravdu Ti to dává smysl?

mystik
Člen | 308
+
0
-

@mbrecher Rozliseni null vs undefined se bezne pouziva v rade jazyku. Proc myslis, ze by se to rozlisovat nemelo?

m.brecher
Generous Backer | 863
+
+2
-

@mystik

Rozliseni null vs undefined se bezne pouziva v rade jazyku. Proc myslis, ze by se to rozlisovat nemelo?

Null by se mělo používat ve významu ‚hodnota neexistuje‘, což je sice stejný význam jako u neinicializované hodnoty ale null a ‚undefined‘ se podstatně liší tím, zda tento stav znamená chybu nebo ne. Null je vědomé nastavení neexistující hodnoty, které v kódu zpracujeme bez výjimky, zatímco neinicializovaná proměnná znamená chybu, která by se měla v kódu opravit. Všichni to takto běžně používáme.

Jenže v aplikacích se často null ve významu ‚neexistující hodnota‘ nepoužívá. Běžně se návrh databáze příliš zjednodušuje a hodnota null pak něco konkrétního znamená, obvykle nahrazuje jedno boolean pole. Příklad:

Striktní přístup – null ve významu ‚hodnota neexistuje‘

public bool $runProcess;		// true = run process, false = do not run process
public ?DateTime $startAt;		// DateTime = datum spuštění, null = datum neexistuje

Zjednodušený přístup – null se skrytým významem ‚do not run process‘

public ?DateTime $startRunAt;	// DateTime = run process + datum spuštění, null = do not run process + datum neexistuje

Ve druhém případě $startRunAt = null má skrytý význam ‚do not run process‘ zatímco ‚undefined‘ nikoliv. V takovém případě se explicitně nastavené null nesmí přepisovat nějakou defaultní hodnotou – to je případ který Jsi zmínil. V prvním případě to nevadí. A protože bezpochyby je druhý způsob použití null běžný, tak je návrh neměnit {default}, rozumný a každý si použije v aplikaci tu variantu, která mu vyhovuje.

David Grudl
Nette Core | 8218
+
+2
-

@mystik

Rozlišování null vs undefined se právě moc neosvědčilo. Protože to zbytečně zvyšuje kognitivní zátěž jazyka.

Peklo je to v JavaScriptu, který má různé typy null a undefined. Například TypeScript coding standards přímo doporučují používat pouze undefined. Ten je v JS přirozený: neexistující proměnná je undefined, funkce bez return vrací undefined, nepředaný parametr funkce je undefined atd. Naopak null je v JS objekt 🤦‍♂️ Komplikací je, že některé funkce, např. z DOM, vrací historicky null.

V PHP je to podobné, jen jazykově nativní je tady null, tj. neexistující proměnná je null, funkce bez return vrací null, atd.

David Grudl
Nette Core | 8218
+
+3
-

Jinak v souvislosti ve šablonama je ještě důležité je tu ještě jedna pikoška. Pokud si kvůli kontrole a napovídání typů vytvořím třídu:

class TemplateParameters
{
	public string $lang;
}

$params = new TemplateParameters;
//$params->lang = 'cs';
$html = $latte->render('template.latte', $params);

…tak pokud nenastavím $params->lang, bude mi fungovat {default} v šabloně. Ale pozor, pokud by property $lang neměla typ:

class TemplateParameters
{
	public $lang;
}

…tak už by {default} nefungoval, protože netypované proměnné mají vždy výchozí hodnotu null, kterou už pak {default} nepřebije.

Kamil Valenta
Člen | 815
+
+3
-

David Grudl napsal(a):

Rozlišování null vs undefined se právě moc neosvědčilo. Protože to zbytečně zvyšuje kognitivní zátěž jazyka.

V jiných textech lze najít, že to naopak kognitivní zátěž snižuje. Bohužel, ať už člověk zastává libovolné paradigma, v obřím internetu najde zdroj, který mu jeho postoj potvrdí…

V PHP je to podobné, jen jazykově nativní je tady null, tj. neexistující proměnná je null, funkce bez return vrací null, atd.

Úplně podobné to není, protože PHP zde opět trochu mlží svou nejednoznačností. Neexistující proměnná není tak úplně null, jen se na null překlápí při přístupu k ní. Ano, další zmatek je, že není null jako null…

Struktura ZVAL totiž obsahuje atribut IS_NULL, který se používá pro definované proměnné s hodnotou null.
Undefined proměnná vůbec svůj ZVAL nemá, proto není tak úplně null. Ale při pokusu vypsat ji se za null nahradí, je to popsáno i v dokumentaci PHP.

Důvod, proč to tak obšírně píšu, je, že to vyjasňuje chování u default hodnot parametrů funkcí. Srovnej:

function foo(string $bar);
function foo(string $bar='default');
function foo(?string $bar);
function foo(?string $bar=null);

V 1. případě není bar undefined nikdy. V 2. být může a pak dostane hodnotu „default“ (null zde není ve hře vůbec), ve 3. případě není nikdy undefined, ale null být může. V posledním případě může být undefined a pak získá defaultní ZVAL IS NULL, nebo bude defined s hodnotou NULL nebo defined s explicitním stringem.

Lette macro {default} se chová jako default parametr funkce. Pokud se držíme toho, že latte je PHP a není jiný nový jazyk, tak je to tak správně.