Proof of concept: Generics

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Filip Procházka
Moderator | 4668
+
0
-

Někteří z nás používají například Doctrine 2 ORM. Doctrine má EntityManager a ten poskytuje pomocí metody $em->getRepository($entity) přístup k repozitářům jednotlivých entit.

Pokud bych chtěl mít repozitáře v DIC, tak není problém, prostě pro každou entitu registruju repozitář

services:
    usersRepository:
        class: Doctrine\ORM\EntityRepository
        factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')

Teď si je můžu v pohodě předávat kam budu potřebovat.

class UsersPresenter extends BasePresenter
{
    public function __construct(Doctrine\ORM\EntityRepository $users) {}

Ale, co kdybych je tam všechny registroval a chtěl si je nechat přes autowiring doplňovat? Nette mi to nedovolí, protože vidí jen XY služeb a všechny mají stejnou třídu. Spousta lidí to „řeší“ tak, že třídu si podědí, pojmenuje ji UsersRepository a tu pak vyžaduje (nebo použije interface). Osobně tohle považuju za anti-pattern a dle mé zkušenosti to není dobrá cesta.

Někteří možná znají ze staticky typovaných jazyků generika – když to hodně zjednoduším, tak jde o to, že si nadefinuji nějakou základní třídu, která „obaluje“ chování nad určitým typem objektu, jako je třeba náš repozitář. Já mu řeknu, že má pracovat s entitou uživatele a on podle toho skládá dotazy.

Když naučíme Nette DIC rozlišovat, s jakým typem služba pracuje, můžeme si ušetřit práci s konfigurací a nechat si vše předat pomocí autowire. Přesně to (mimo jiné) dělá tento pull request.

Upravím si tedy definici služby

services:
    usersRepository:
        class: Doctrine\ORM\EntityRepository<Acme\User>
        factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')

a doplním annotaci do presenteru

class UsersPresenter extends BasePresenter
{
    /** @param Doctrine\ORM\EntityRepository<Acme\User> $users */
    public function __construct(Doctrine\ORM\EntityRepository $users) {}

V tenhle moment už Nette umí rozlišit, která služba je která a správně je automaticky doplní.

Připravil jsem funkční pull request jako proof of concept a rád bych se dozvěděl, co si o tom myslí komunita a David :)

Samozřejmě je potřeba se smířit s faktem, že bez annotací to nepůjde. Tedy pokud někdo nevymyslí lepší implementaci :)

Editoval HosipLan (4. 7. 2012 18:48)

Honza Marek
Člen | 1664
+
0
-

Moc se mi to nezdá, protože Doctrine\ORM\EntityRepository<Acme\User> neznamená v PHP vůbec nic. Ba dokonce to není ani třída, takže to zanáší bordel jak do konfigurace kontejneru, tak do PHPDocu.

Filip Procházka
Moderator | 4668
+
0
-

Neřekl bych zrovna bordel, ale je to dost nestandardní, to ano.

Původně jsem to chtěl dělat jako rozšíření, ale narazil jsem na strašně moc překážek. Teď, když mi to funguje, zkusím z toho znovu udělat jako rozšíření. Třeba to tentokrát vymyslím lépe. Taky bych to asi nechtěl přímo v Nette.

//edit: Byl jsem opraven, že generika je trošku širší pojem:) S čímž souhlasím, já s z toho vzal jen to co se hodilo :)

Editoval HosipLan (4. 7. 2012 22:13)

llook
Člen | 407
+
0
-

Přijde mi to moc magické a hlavně mám problém s tím, že se to tváří jako něco jiného. Jestli to dobře chápu, tak jde o jakési značkování služeb. Na značkování služeb máme tagy, mě by se víc líbil podobný zápis:

services:
    usersRepository:
        class: Doctrine\ORM\EntityRepository
        factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')
        tags: [ "Acme\User" ]

Potom zbývá druhá polovina problému: Naučit autowiring předávat služby podle typu a tagu (nějaké hypotetické getByTypeAndTag()). Určitě bych zvolil jinou syntaxi. Takhle to vypadá jako generický typ a to mate. Také je potřeba zkusit, jak si s tím nestandardním @param poradí různá IDE, Apigen a Docblox.

Ale hlavně si nejsem jistý, jestli to je opravdu lepší, než podědění třídy EntityRepository. Proč je to podle tebe anti-pattern?

Filip Procházka
Moderator | 4668
+
0
-

K tématu:

@kaja47 napsal(a):

Nesnažím se upřesnit názvosloví, jenom pozoruji.

Ty potřebuješ, aby ti DIC automaticky servíroval různé instance jednoho typu a aby je od sebe rozpoznal, tak používá nějaký tag, v tomhle případě typ objektu se kterým EntityRepository pracuje. Ale tahle asociace není nijak zásadní, je jenom užitečná, protože typ entity v tomhle konkrétním případě rozlišuje jednotlivé instance EntityRepository, ale EntityRepository<E> nemá žádný význam a nepřinese nic jiného kromě práce DIC.

Ke generikům by to mělo mnohem blíž, kdyby sis mohl nadefinovat jednu generickou službu,

services:
  repository<E>:
    class: Doctrine\ORM\EntityRepository<E>
    factory: @\Doctrine\ORM\EntityManager::getRepository(get_class(<E>))

A anotace @param Doctrine\ORM\EntityRepository<Acme\HorseEtity> by udělala správnou věc ™.

S generickými typy pak taky souvisí veselé pojmy jako kovariance a kontravariance a rozlišování, co je podtyp čeho a to není vůbec vůbec jednoduchý :).

Ale teď, když o tom tak přemýšlím, tak vlastně tvoje implementace vlastně dělá přesně to, co jsem napsal nahoře: typu, který má být dodán DICem, ve špičatých závorkách dáváš nějaký tag, který vůbec nemusí být jméno typu/třídy, ale naprosto cokoli.

„Generická služba“ je to, co kdyby z tohohle vzniklo, tak by to bylo úplně nejlepší.


llook napsal(a):

Přijde mi to moc magické a hlavně mám problém s tím, že se to tváří jako něco jiného. Jestli to dobře chápu, tak jde o jakési značkování služeb. Na značkování služeb máme tagy, mě by se víc líbil podobný zápis:

Chápeš to dobře. Zápisů v neonu jde vymyslet mraky. A ano, v současnosti je to v podstatě tag.

Potom zbývá druhá polovina problému: Naučit autowiring předávat služby podle typu a tagu (nějaké hypotetické getByTypeAndTag()). Určitě bych zvolil jinou syntaxi. Takhle to vypadá jako generický typ a to mate.

Podobnost s generickým typem jsem zvolil záměrně, protože z toho v podstatě vycházím.

Také je potřeba zkusit, jak si s tím nestandardním @param poradí různá IDE, Apigen a Docblox.

Schválně jsem ten regulár napsal tak, aby nebyl moc pendant a v pohodě tedy můžu psát cokoliv z následujícího.

@param Foo<Bar>
@param Foo <Bar>
@param Foo <Bar> $foo
@param Foo $foo <Bar>

Osobně testuji třetí zápis a IDE to vubec nevadí, dokonce když dám generovat phpDoc, tak mi to tam nechá, protože to považuje za „poznámku“ :)

Ale hlavně si nejsem jistý, jestli to je opravdu lepší, než podědění třídy EntityRepository. Proč je to podle tebe anti-pattern?

Vycházím z tohoto článku. A ano, jsem si skálopevně jistný, že to co se snažím dosáhnout je ta správná cesta™.


Ještě bych to tedy shrnul. Zápis v Neonu bych moc neřešil. Považuji ho za správný, protože ho budu chtít rozšířit na to, co popisuje @kaja47 výše. Chtěl bych se generice přiblížit, ale v jednodušší formě. Prostě jen přidat službě tento „chytřejší tag“.

Zrovna mě třeba napadá to znásilnit na chytrou factory

factories:
    usersRepository:
        class: Doctrine\ORM\EntityRepository
        factory: @\Doctrine\ORM\EntityManager::getRepository(%type%)
        tags: [ generic ]

ale uvidím, ještě nad tím budu přemýšlet :) Cest jak to implementovat je spousta. Raději bych ale, aby to připomínalo zápis z jiných jazyků, protože se snažím o podobnou věc.

Co mě ale trápí, tak je jak to zapsat v tříde, do které chci injektovat. Já to nemám problém překousnout, ale od ostatních jsem na to ještě nedostal jedinou pozitivní reakci. Asi to bude tím, že pracuju s Doctrine :)

Napadá mě, zavést úplně novou annotaci. Něco jako:

/**
 * @genericTypeParameter($bar, Bar<Fuuuu>)
 * @genericTypeParameter($baz, Baz<Fuuuu>)
 *
 * @param Bar $bar
 * @param BaZ $baz
 */
public function foo(Bar $bar, Baz $baz) { }

Pokud by někdo měl pěkný nápad, sem s ním :)

Editoval HosipLan (5. 7. 2012 12:06)

Pavel Kouřil
Člen | 128
+
0
-

No; mě se to jako nápad rozhodně líbí. :)

Co mi ale vadí na tom @param Foo<Bar> je to, že ty do třídy (a hlavně teda určité standardní anotace) dáváš něco, co způsobuje svým způsobem závislost na určitém kontejneru.

Mnohem víc se mi líbí tedy nějaká anotace ve stylu @Nette\GenericTypeParameter($bar, Foo<Bar>).

Filip Procházka
Moderator | 4668
+
0
-

@Pajka: já to raději považuji za „doplňující meta informaci“, která mi usnadňuje používání jednoho konkrétního containeru :) Prostě rozšíření autowiringu. Kdekoliv jinde bych ty služby prostě vyjmenoval, tohle je výrazné ulehčení.

Filip Procházka
Moderator | 4668
+
0
-

Update: nový zápis

services:
  repository<E>:
    class: Doctrine\ORM\EntityRepository
    factory: @\Doctrine\ORM\EntityManager::getRepository(<E>)

to <E> v názvu služby je kratší zápis pro

services:
  repository:
    class: Doctrine\ORM\EntityRepository
    generic: E # označuje název "parametru s typem"
    factory: @\Doctrine\ORM\EntityManager::getRepository(<E>)

Jak je vidět v testech, fungují nové zápisy.

$container->getService('dao', 'User');
$container->getService('dao', 'Article')

$container->getService('dao<User>');
$container->getService('dao<Article>');

$container->getByType('Generic_IDao<User>');
$container->getByType('Generic_IDao<Article>');

Taky jsem si říkal, jestli povolovat oba zápisy, nebo jeden vyhodit, popř doplnit druhý zápis do getByType()

Teď už to má ke generice trošku blíže. Budu vděčný za další feedback :)

Editoval HosipLan (5. 7. 2012 17:50)

kaja47
Člen | 16
+
0
-

Za mě dobrý, jenom bych tomu neříkal generické typy (protože to generické typy nejsou). Možná generické služby.

Kdyby ses vydal cestou typů, pak musíš vyřešit několik zásadních otázek:

Když mám definovanou službu S<X> a chci S, dostanu S<X>?
Když mám definovanou službu S a chci S<X>, dostanu obcenou službu X?

Správná odpověď je: „ne“, protože jde o jiné typy (ve smyslu Unboxed newtype).

Ale hlavně to nejdůležitější:

Když mám definovanou službu S<Child> a chci S<Parent>, bude to fungovat (dostanu S<Child>)?

Na první pohled to nevypadá nijak složitě. S<Child> více specifický než S<Parent>, takže když chci něco obecného a dostanu konkrétní implementaci, je všechno v pořádku.

Tak to funguje u tříd, které „produkují“ svůj typový parametr <T>. Když ho konzumují, je to naopak.

Když mám třídu Serializer<T>, která vezme nějaké T a serializuje ho. Mám definovanou službu Serializer<Pony> a někde chci instanci Serializer<Animal>. Když mi DIC dodá Serializer<Pony> (tedy více specifickou třídu) nebude to fungovat, protože já chci serializovat všechny zvířata, ne jenom poníky.

Dává to smysl? Ne? Vítej do světa kovariance a kontravariance, jednoho z nejvíce matoucích pojmů v programování.

Propaguj to jako glorifikovaný tag. Užitečné to bude i bez předstírání generik:

např. mám dvě připojení do databáze: jedno pro archiv a druhé pro živou db, definuji si dvě služby Connection<live> a Connection<archive> a můžu si je nechat injectovat dle libosti.

Filip Procházka
Moderator | 4668
+
0
-

U těch rodičů a dětí už jsem ztratil :) Ale dejme tomu, generické služby u mně dobrý :)

Když mám definovanou službu S<X> a chci S, dostanu S<X>?

výjimka, nespecifikoval jsi X pro S

Když mám definovanou službu S a chci S<X>, dostanu obcenou službu X?

výjimka, služba není označena jako generická.

Editoval HosipLan (6. 7. 2012 9:24)

juzna.cz
Člen | 248
+
0
-

Ze zkušeností s Javou vím, že generika jsou fakt super věc. Základ je navíc docela jednoduchý na pochopení, ale pak nastanou situace (např. které zmiňuje kaja47) a tam se to začíná celé dost komplikovat. Bohužel bez přesného vyřešní i těch komplikovaných situací se můžeme dostat do slepé uličky.

Mě by se spíše daleko více než DIC líbila podpora v IDE, tzn. aby fungovalo napovídání u $em->getRepository('Acme\User')->find($userId)->getXYZ – to je zase ale jiná story.

A s tím, co „něco znamená v PHP“, bych si až tak hlavu nelámal – už teď se v PHP používá hodně věcí, které nic neznamenají, např. již věrně známé anotace v komentářích nebo zápisy pole Foo[].

Editoval juzna.cz (9. 7. 2012 14:41)

Filip Procházka
Moderator | 4668
+
0
-

Napadá tě jak to řešit? Já v tomhle nemám žádné zkušenosti :)

Editoval HosipLan (9. 7. 2012 14:54)

juzna.cz
Člen | 248
+
0
-

Pajka napsal(a):

Co mi ale vadí na tom @param Foo<Bar> je to, že ty do třídy (a hlavně teda určité standardní anotace) dáváš něco, co způsobuje svým způsobem závislost na určitém kontejneru.

Tady nezavádíme zádnou závistlost na konkrétní implementaci čehokoliv – jedná se o univerzálně použitelný zápis typu, kterému následně může nějaký DIC rozumět.

Podobně byl zaveden a už se docela ujal zápis polí a'la Foo[] místo příliš obecného array. Původní zápis, který je navíc validní v PHP, nebyl dostatečně popisný. Už jsme to zlepšili částečně, tak proč to nezlepšit ještě více? Když někdo (presenter, služba, …) potřebuje repozitář uživatelů, může si o něj říci rovnou. To je lepší, než si říct jen o obecný repozitář a pak kontrolovat, jestli opravdu obsahuje uživatele. Navíc se tím jasně deklaruje závislost.

Pole něčeho jsou vlastně jednou konkrétní podčástí obecného problému generických typů. PHP samotné generická pole neumí, takže když implementujeme mixér na pomeranče, píšeme:

/**
 * @param  Orange[] hromádka pomerančů
 * @return Juice pomarančový džus
 */
 public function mix(array $items) {}

Supr, né? Klasický komentářový hack, na který jsme v PHP zvyklí.

Když budeme aplikovat stejný postup ještě dále, dostaneme o trochu větší třídu generických typů:

/**
 * @param  EntityRepository <Orange> chytřejší hromádka pomerančů
 * @return Juice <Orange> pomarančový džus
 */
 public function mix(EntityRepository $items) {}

Opět, co nejde zapsat nativně, musíme dát do anotací. Z pohledu do kódu nebo do API dokumentace je však jasné, jaký že to repozitář máme mixéru dát. Už se tedy nikdo nebude ptát jaký repozitář že to má do mixéru dát (stejně jako dříve se ptal „a to tam mám dát jako pole čeho???“).

Pozn.: V Javě by se psalo Juice<Orange> (bez mezery), což by ale rozbilo všechny PHP IDE. Zápis s mezerou je nerozbije.

Další výhoda nastane při popisu mě samého ;)

class Juzna
{

	/**
	 * @param  Juice <Orange> juzna nepije jiné druhy džusu!
	 */
	public function __construct(Juice $drink) { ... }

}

Teď vím nejen jaký repozitář dát do mixéru, ale i že to juzna nenaštve.

Summary: jde tedy o to, abychom mohli detailněji specifikovat závislosti (ve smyslu DI). A když jim pak bude rozumnět i DIC, tak to bude supr.

Editoval juzna.cz (9. 7. 2012 15:32)

David Grudl
Nette Core | 8082
+
0
-

To je šikovné. Jen implementace by měla být jiná. Pokud možno bez zásahů do Reflection a NeonAdapteru.

Filip Procházka
Moderator | 4668
+
0
-

Reflexi jsem v podstatě jen rozšířil a opravil tam pár věcí, co by se daly považovat za bugy. Jestli chceš, pošlu ti tyhle dva commity jako pullrequest zvlášť.

Co se týče toho ostatního, teď z fleku nevím jak to napsat lépe. Pokud tě něco napadá tak to rád předělám :)

Každopádně, už víc jak týden to spokojeně používám a chrochtám blahem :)

llook
Člen | 407
+
0
-

Ten nový zápis vypadá líp, ale nelíbí se mi, že ta služba má s každým parametrem deklarovanou stejnou třídu.

Snad by to šlo řešit tím, že by se ten parametr mohl použít i jako součást názvu třídy:

repository<P>:
	class: model\<P>Repository
	factory: @orm::getRepository( <P> )

Kontejner by při volání getByType('model\CokoliRepository') našel shodu s tou maskou, takže by se odvozené repositories mohly injektovat tím úplně nejméně WTF způsobem:

public function __construct(UsersRepository $users)
{
	$this->users = $users;
}

Ale nejsem si jistý, nakolik je vůbec něco podobného potřeba a nakolik jde o zbytečné nabývání syntaxe. Pokud by to bylo v oficiálním Nette, tak bych to používal, ale docela dobře se bez toho obejdu.

Filip Procházka
Moderator | 4668
+
0
-

Asi chápu, o co ti jde (napovídání typů výsledků metod). Ale to co chceš jde proti tomu, o co se snažím a proti čistému kódu. Už jsem ti to říkal jednou.

Celé tohle dělám, abych ty repozitáře dědit nemusel. Kdybych je chtěl dědit, tak nevymýšlím generické služby.

//edit:
Už jsem si to přečetl pořádně :) Automatické děděni (aby to generoval container) jsem zvažoval hned na začátku, ale pak jsem to zavrhl, protože i ty annotace jsou podle mě lepší.

Navíc vem si, taková doctrine… Já si automaticky podědím třídy, ale pak je budu muset zase nastavovat entitám. To je strašně složité. Ale dejme tomu, nějak to ohackuju… Ale co když pak opravdu u jedné chtít udělat výjimku a opravdu ten repozitář podědím, nebo někdo jiný.. jak to pak dostanu do té Doctrine?

Tohle podle mně není řešení.


Padl nápad místo dědění použít class_alias(). Což sice zní rozumně, ale napovídání to nepomůže…


Co se týče toho napovídání typů, dědění a upravování annotací je jediná možnost, jak toho se současnými prostředky dosáhnout automatizovaně.

Editoval HosipLan (19. 7. 2012 15:27)

Honza Marek
Člen | 1664
+
0
-

Alternativní řešení: Autowiring by mohl v případě nejasnosti podle typu zohledňovat i název proměnné a cpát tam stejně pojmenovanou službu (nebo koneckonců v případě skalární proměnné i parametr).

pekelnik
Člen | 462
+
0
-

Autowiring by mohl v případě nejasnosti podle typu zohledňovat i název proměnné a cpát tam stejně pojmenovanou službu (nebo koneckonců v případě skalární proměnné i parametr).

Nad timhle jsem se take zamyslel – zni to silene ale bylo by to super :)

awsickness
Člen | 98
+
0
-

ohledne tech skalarnich promenych jsem za.
to by se mi hodne libilo.

blindAlley
Člen | 31
+
0
-

Možná by ta úprava autowiringu navrhovaná H. Markem mohla řešit i problém https://forum.nette.org/…z-type-hintu. Asi by to chtělo promyslet ty pravidla pro jména parametrů aby se tím pokryly všechny možné zápisy služeb v DIC, i ty, co jsou registrovány přes nějaká DIC rozšíření. Jsem pro tuto alternativu.

Filip Procházka
Moderator | 4668
+
0
-

Názvy parametrů možná, ale je to hnusná magie, u které bude víc WTF než užitku. Takhle mě to vždy seřve, že mi chybí parametr a já si ho doplním. To je 1000× lepší, než kdyby se mi tam předal sám a já si nebyl vůbec ničím jistý, že se tam předává ten správný. Přijde po mě nějaký vtipálek, zrefaktoruje mi třídu a najednou to přestane fungovat, nebo hůř, začne se to chovat nepředvídatelně a nikdo to nepozná, dokud to nezničí data na produkci.

A ještě k těm službám: NE. Názvy služeb vůbec třídy nezajímají. Třídy ani neví, že nějaké služby existují! Ani v annotacích! To že to tak dělá Java, není žádný argument.

Editoval HosipLan (3. 8. 2012 11:56)

David Ďurika
Člen | 328
+
0
-

HosipLan napsal(a):

Někteří z nás používají například Doctrine 2 ORM. Doctrine má EntityManager a ten poskytuje pomocí metody $em->getRepository($entity) přístup k repozitářům jednotlivých entit.

Pokud bych chtěl mít repozitáře v DIC, tak není problém, prostě pro každou entitu registruju repozitář

services:
    usersRepository:
        class: Doctrine\ORM\EntityRepository
        factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')

Teď si je můžu v pohodě předávat kam budu potřebovat.

class UsersPresenter extends BasePresenter
{
    public function __construct(Doctrine\ORM\EntityRepository $users) {}

Ale, co kdybych je tam všechny registroval a chtěl si je nechat přes autowiring doplňovat? Nette mi to nedovolí, protože vidí jen XY služeb a všechny mají stejnou třídu. Spousta lidí to „řeší“ tak, že třídu si podědí, pojmenuje ji UsersRepository a tu pak vyžaduje (nebo použije interface). Osobně tohle považuju za anti-pattern a dle mé zkušenosti to není dobrá cesta.

tento problem by sa dal elegantne vyriesit cez accessoy a dokonca by to bolo aj lazy

norbe
Backer | 405
+
0
-

V jakém stavu se tohle nachází? Koukal jsem na github a tam jsou dva pull requesty, oba zavřené, tak jestli má cenu to nějak dál zkoumat…

Filip Procházka
Moderator | 4668
+
0
-

Zavrhl jsem to, protože je to příliš složité. Řeším to tak, že generické služby emuluju pomocí továrniček, ale musím kvůli tomu pak předání služby konfigurovat.

services:
	articles: App\Articles(@doctrine.dao(App\Article))

factories:
	doctrine.dao:
		create: @\Doctrine\ORM\EntityManager::getRepository(%entityName%)
		parameters: [entityName]