Programování

Podívejte se na sílu parametrického polymorfismu

Předpokládejme, že chcete implementovat třídu seznamu v Javě. Začnete abstraktní třídou, Seznama dvě podtřídy, Prázdný a Nevýhody, představující prázdné a neprázdné seznamy. Protože plánujete rozšířit funkčnost těchto seznamů, navrhujete a ListVisitor rozhraní a poskytnout akceptovat(...) háčky pro ListVisitors v každé z vašich podtříd. Dále vaše Nevýhody třída má dvě pole, První a zbytek, s odpovídajícími metodami přístupového objektu.

Jaké budou typy těchto polí? Jasně, zbytek by měl být typu Seznam. Pokud předem víte, že vaše seznamy budou vždy obsahovat prvky dané třídy, bude úloha kódování v tomto okamžiku podstatně snazší. Pokud víte, že všechny prvky seznamu budou celé číslonapříklad můžete přiřadit První být typu celé číslo.

Pokud však tyto informace neznáte předem, jak je tomu často, musíte se spokojit s nejméně běžnou nadtřídou, která obsahuje všechny možné prvky obsažené ve vašich seznamech, což je obvykle univerzální referenční typ. Objekt. Váš kód pro seznamy prvků různých typů má tedy následující podobu:

abstraktní třída List {public abstract Object accept (ListVisitor that); } interface ListVisitor {public Object _case (Empty that); public Object _case (Nevýhody); } třída Empty rozšiřuje List {public Object accept (ListVisitor that) {return that._case (this); }} třída Cons nejprve rozšiřuje List {private Object; odpočinek na soukromém seznamu; Nevýhody (Object _first, List _rest) {first = _first; rest = _rest; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Přestože programátoři Java tímto způsobem často pro pole používají nejméně běžnou nadtřídu, přístup má své nevýhody. Předpokládejme, že vytvoříte ListVisitor který přidá všechny prvky ze seznamu Celé číslosa vrátí výsledek, jak je znázorněno níže:

třída AddVisitor implementuje ListVisitor {soukromé celé číslo nula = nové celé číslo (0); public Object _case (Empty that) {return zero;} public Object _case (Cons that) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (this)). intValue ()); }} 

Všimněte si explicitního obsazení Celé číslo ve druhém _případ(...) metoda. Opakovaně provádíte běhové testy, abyste zkontrolovali vlastnosti dat; v ideálním případě by překladač měl tyto testy provést za vás jako součást kontroly typu programu. Ale protože to nemáte zaručené AddVisitor bude použito pouze pro Seznams Celé číslos, kontrola typu Java nemůže potvrdit, že ve skutečnosti přidáváte dva Celé číslos pokud nejsou přítomny obsazení.

Mohli byste potenciálně získat přesnější kontrolu typu, ale pouze obětováním polymorfismu a duplikování kódu. Můžete například vytvořit speciální Seznam třída (s odpovídajícím Nevýhody a Prázdný podtřídy, stejně jako speciální Návštěvník rozhraní) pro každou třídu prvků, které ukládáte do a Seznam. Ve výše uvedeném příkladu byste vytvořili IntegerList třída, jejíž prvky jsou všechny Celé číslos. Ale pokud jste chtěli uložit, řekněme Booleovskýs na nějakém jiném místě v programu, budete muset vytvořit a BooleanList třída.

Je zřejmé, že velikost programu napsaného pomocí této techniky by se rychle zvýšila. Existují také další stylistické problémy; jedním ze základních principů dobrého softwarového inženýrství je mít jeden bod kontroly pro každý funkční prvek programu a duplikování kódu tímto způsobem kopírování a vkládání tento princip porušuje. Obvykle to vede k vysokým nákladům na vývoj a údržbu softwaru. Chcete-li zjistit, proč, zvažte, co se stane, když je chyba nalezena: programátor by se musel vrátit zpět a opravit tuto chybu zvlášť v každé vytvořené kopii. Pokud programátor zapomene identifikovat všechny duplikované stránky, bude zavedena nová chyba!

Ale jak ilustruje výše uvedený příklad, zjistíte, že je obtížné současně udržovat jediný kontrolní bod a používat kontroly statického typu k zajištění toho, že při spuštění programu nikdy nedojde k určitým chybám. V Javě, jak existuje dnes, často nemáte jinou možnost, než duplikovat kód, pokud chcete přesnou kontrolu statického typu. Pro jistotu byste tento aspekt Javy nikdy nemohli úplně vyloučit. Některé postuláty teorie automatů, přijaté k jejich logickému závěru, naznačují, že žádný systém zvukového typu nedokáže přesně určit soubor platných vstupů (nebo výstupů) pro všechny metody v programu. V důsledku toho musí každý typový systém najít rovnováhu mezi svou vlastní jednoduchostí a expresivitou výsledného jazyka; systém typu Java se příliš naklání ve směru jednoduchosti. V prvním příkladu by vám systém o něco expresivnějšího typu umožnil udržovat přesnou kontrolu typu, aniž byste museli duplikovat kód.

Takový expresivní systém by přidal generické typy do jazyka. Obecné typy jsou proměnné typu, které lze vytvořit instancí s vhodně specifickým typem pro každou instanci třídy. Pro účely tohoto článku budu deklarovat proměnné typu v úhlových závorkách nad definicemi třídy nebo rozhraní. Rozsah proměnné typu pak bude sestávat z těla definice, ve které byla deklarována (bez zahrnutí rozšiřuje doložka). V rámci tohoto oboru můžete použít proměnnou typu kdekoli, kde můžete použít běžný typ.

Například u obecných typů můžete přepsat svůj Seznam třída takto:

abstraktní třída List {public abstract T přijmout (ListVisitor že); } interface ListVisitor {public T _case (Empty that); public T _case (Nevýhody); } třída Empty rozšiřuje List {public T accept (ListVisitor that) {return that._case (this); }} třída Cons nejprve rozšiřuje List {private T; odpočinek na soukromém seznamu; Nevýhody (T _ první, seznam _ odpočinek) {první = _ první; rest = _rest; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Nyní můžete přepsat AddVisitor využít výhod obecných typů:

třída AddVisitor implementuje ListVisitor {soukromé celé číslo nula = nové celé číslo (0); public Integer _case (Empty that) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Všimněte si, že explicitní obsazení Celé číslo již nejsou potřeba. Argument že do druhého _případ(...) metoda je deklarována jako Nevýhody, vytvoření instance proměnné typu pro Nevýhody třída s Celé číslo. Kontrola statického typu to proto může dokázat that.first () bude typu Celé číslo a to that.rest () bude typu Seznam. Podobné instance budou vytvořeny pokaždé, když dojde k nové instanci Prázdný nebo Nevýhody je deklarováno.

Ve výše uvedeném příkladu lze proměnné typu vytvořit s libovolnou instancí Objekt. Můžete také zadat konkrétnější horní mez typu proměnné. V takových případech můžete tuto vazbu uvést v deklaračním bodě proměnné typu s následující syntaxí:

  rozšiřuje 

Například pokud jste chtěli svůj Seznams obsahovat pouze Srovnatelný objekty, můžete definovat své tři třídy takto:

třída Seznam {...} třída Nevýhody {...} třída Prázdná {...} 

Ačkoli přidání parametrizovaných typů do Javy by vám poskytlo výhody uvedené výše, nebylo by to užitečné, kdyby to znamenalo obětovat kompatibilitu se starým kódem v procesu. Naštěstí taková oběť není nutná. Je možné automaticky přeložit kód napsaný v rozšíření Java, které má obecné typy, do bytecode pro stávající JVM. Několik překladačů to již dělá - zvláště dobrým příkladem jsou překladače Pizza a GJ, které napsal Martin Odersky. Pizza byl experimentální jazyk, který do Javy přidal několik nových funkcí, z nichž některé byly začleněny do Javy 1.2; GJ je nástupcem společnosti Pizza, která přidává pouze obecné typy. Jelikož se jedná o jedinou přidanou funkci, kompilátor GJ může vytvářet bytecode, který hladce funguje se starým kódem. Kompiluje zdroj do bytecode pomocí vymazání typu, který nahradí každou instanci každé proměnné typu horní mezí této proměnné. Umožňuje také deklarovat proměnné typu pro konkrétní metody, nikoli pro celé třídy. GJ používá stejnou syntaxi pro obecné typy, které používám v tomto článku.

Probíhající práce

Na Rice University skupina pro programovací jazyky, ve které pracuji, implementuje kompilátor pro vzestupně kompatibilní verzi GJ s názvem NextGen. Jazyk NextGen společně vyvinuli profesor Robert Cartwright z oddělení výpočetní techniky Rice a Guy Steele ze Sun Microsystems; přidává do GJ možnost provádět běhové kontroly proměnných typu.

Další potenciální řešení tohoto problému, nazvané PolyJ, bylo vyvinuto na MIT. V Cornellu se rozšiřuje. PolyJ používá mírně odlišnou syntaxi než GJ / NextGen. Také se mírně liší v použití obecných typů. Například nepodporuje parametrizaci typů jednotlivých metod a aktuálně nepodporuje vnitřní třídy. Ale na rozdíl od GJ nebo NextGen umožňuje instanci proměnných typů s primitivními typy. Stejně jako NextGen podporuje PolyJ runtime operace na obecných typech.

Společnost Sun vydala žádost o specifikaci Java (JSR) pro přidávání obecných typů do jazyka. Není překvapením, že jedním z klíčových cílů uvedených pro jakékoli podání je údržba kompatibility se stávajícími knihovnami tříd. Když se do Javy přidají obecné typy, je pravděpodobné, že jeden z výše diskutovaných návrhů poslouží jako prototyp.

Existují někteří programátoři, kteří jsou proti přidávání generických typů v jakékoli formě, navzdory jejich výhodám. Budu hovořit o dvou běžných argumentech takových oponentů, jako je argument „šablony jsou zlé“ a argument „není to objektově orientovaný“, a postupně se budu zabývat každým z nich.

Jsou šablony zlé?

C ++ používá šablony poskytnout formu obecných typů. Šablony si u některých vývojářů C ++ získaly špatnou pověst, protože jejich definice nejsou typově kontrolovány v parametrizované formě. Místo toho se kód replikuje při každém vytvoření instance a každá replikace se typem kontroluje samostatně. Problém s tímto přístupem spočívá v tom, že chyby typu mohou existovat v původním kódu, které se nezobrazí v žádném z počátečních instancí. Tyto chyby se mohou projevit později, pokud revize programu nebo rozšíření zavedou nové instance. Představte si frustraci vývojáře používajícího existující třídy, které zadávají kontrolu, když jsou kompilovány sami, ale ne poté, co přidá novou, naprosto legitimní podtřídu! Ještě horší je, že pokud šablona nebude znovu zkompilována spolu s novými třídami, takové chyby nebudou detekovány, ale místo toho poškodí prováděcí program.

Kvůli těmto problémům se někteří lidé zamračili při vracení šablon zpět a očekávali, že se nevýhody šablon v C ++ použijí na systém obecného typu v Javě. Tato analogie je zavádějící, protože sémantické základy Java a C ++ se radikálně liší. C ++ je nebezpečný jazyk, ve kterém je statická kontrola typu heuristickým procesem bez matematického základu. Naproti tomu Java je bezpečný jazyk, ve kterém kontrola statického typu doslova dokazuje, že při provádění kódu nemůže dojít k určitým chybám. Výsledkem je, že programy C ++ zahrnující šablony trpí nesčetnými bezpečnostními problémy, které se v Javě nemohou vyskytnout.

Kromě toho všechny prominentní návrhy pro obecnou Javu provádějí explicitní kontrolu statického typu parametrizovaných tříd, spíše než to dělají jen v každé instanci třídy. Pokud se obáváte, že by taková explicitní kontrola zpomalila kontrolu typu, buďte si jisti, že ve skutečnosti je pravý opak: protože kontrola typu provede pouze jeden průchod parametrizovaným kódem, na rozdíl od průchodu pro každou instanci parametrizovaných typů je proces kontroly typů urychlen. Z těchto důvodů se četné námitky k šablonám C ++ nevztahují na návrhy obecného typu pro Javu. Ve skutečnosti, pokud se podíváte nad rámec toho, co se v průmyslu široce používá, existuje mnoho méně populárních, ale velmi dobře navržených jazyků, jako jsou Objective Caml a Eiffel, které s velkou výhodou podporují parametrizované typy.

Jsou systémy obecného typu objektově orientované?

Nakonec někteří programátoři namítají proti jakémukoli systému obecného typu z toho důvodu, že protože tyto systémy byly původně vyvinuty pro funkční jazyky, nejsou objektově orientované. Tato námitka je falešná. Generické typy velmi přirozeně zapadají do objektově orientovaného rámce, jak ukazují příklady a diskuse výše. Mám ale podezření, že tato námitka má kořeny v nedostatečném pochopení toho, jak integrovat generické typy s polymorfismem dědičnosti Javy. Ve skutečnosti je taková integrace možná a je základem pro naši implementaci NextGen.