Programmering

Se styrken ved parametrisk polymorfisme

Antag, at du vil implementere en listeklasse i Java. Du starter med en abstrakt klasse, Listeog to underklasser, Tom og Ulemper, der repræsenterer henholdsvis tomme og ikke-lette lister. Da du planlægger at udvide funktionaliteten på disse lister, designer du en ListVisitor interface og give acceptere(...) kroge til ListVisitors i hver af dine underklasser. Desuden din Ulemper klasse har to felter, først og hvilemed tilsvarende tilgangsmetoder.

Hvad vil typerne af disse felter være? Klart, hvile skal være af typen Liste. Hvis du på forhånd ved, at dine lister altid vil indeholde elementer fra en given klasse, bliver kodningen meget lettere på dette tidspunkt. Hvis du ved, at dine listeelementer alle vil være heltals, for eksempel, kan du tildele først at være af typen heltal.

Men hvis du, som det ofte er tilfældet, ikke kender disse oplysninger på forhånd, skal du nøjes med den mindst almindelige superklasse, der har alle mulige elementer indeholdt i dine lister, hvilket typisk er den universelle referencetype Objekt. Derfor har din kode til lister med forskellige elementelementer følgende form:

abstrakt klasseliste {offentlig abstrakt Objekt accepter (ListVisitor det); } grænseflade ListVisitor {public Object _case (Tøm det); public Object _case (Cons that); } klasse Empty extends List {public Object accept (ListVisitor that) {return that._case (this); }} klasse Ulemper udvider listen {privat objekt først; privat liste resten; Ulemper (Object _first, List _rest) {first = _first; hvile = _rest; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Selvom Java-programmører ofte bruger den mindst almindelige superklasse til et felt på denne måde, har tilgangen sine ulemper. Antag at du opretter en ListVisitor der tilføjer alle elementerne i en liste over Heltals og returnerer resultatet som illustreret nedenfor:

klasse AddVisitor implementerer ListVisitor {privat heltal nul = nyt heltal (0); public Object _case (Empty that) {return zero;} public Object _case (Cons that) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (dette)). intValue ()); }} 

Bemærk de eksplicitte rollebesætninger til Heltal i det andet _sag(...) metode. Du udfører gentagne gange runtime-tests for at kontrollere dataens egenskaber; ideelt set skal compileren udføre disse tests for dig som en del af programtypekontrol. Men da du ikke er garanteret det AddVisitor vil kun blive anvendt på Listes af Heltals, kan Java-typekontrollen ikke bekræfte, at du faktisk tilføjer to Heltals medmindre kasterne er til stede.

Du kan muligvis opnå mere præcis typekontrol, men kun ved at ofre polymorfisme og duplikere kode. Du kan for eksempel oprette en special Liste klasse (med tilsvarende Ulemper og Tom underklasser samt en special Besøgende interface) for hver klasse af element, du gemmer i en Liste. I eksemplet ovenfor opretter du en Heltalliste klasse, hvis elementer er alle Heltals. Men hvis du ville gemme, så sig Boolsks et andet sted i programmet, skal du oprette en BooleanList klasse.

Det er klart, at størrelsen på et program, der er skrevet ved hjælp af denne teknik, vil stige hurtigt. Der er også yderligere stilistiske spørgsmål; et af de væsentligste principper for god softwareudvikling er at have et enkelt kontrolpunkt for hvert funktionelle element i programmet, og duplikering af kode på denne måde at kopiere og indsætte overtræder dette princip. Dette fører ofte til høje softwareudviklings- og vedligeholdelsesomkostninger. For at se hvorfor skal du overveje, hvad der sker, når der findes en fejl: programmøren bliver nødt til at gå tilbage og rette den fejl separat i hver kopi, der laves. Hvis programmøren glemmer at identificere alle de duplikerede websteder, introduceres en ny fejl!

Men som eksemplet ovenfor illustrerer, vil du have svært ved samtidig at beholde et enkelt kontrolpunkt og bruge statiske type checkers for at garantere, at visse fejl aldrig opstår, når programmet udføres. I Java, som det eksisterer i dag, har du ofte intet andet valg end at duplikere kode, hvis du vil have præcis statisk typekontrol. For at være sikker kunne du aldrig helt fjerne dette aspekt af Java. Visse postulater fra automatteori, taget til deres logiske konklusion, antyder, at intet lydtypesystem kan bestemme nøjagtigt sættet med gyldige indgange (eller output) for alle metoder i et program. Derfor skal alle typer systemer finde en balance mellem sin egen enkelhed og det resulterende sprogs ekspressivitet; Java-typesystemet læner sig lidt for meget i retning af enkelhed. I det første eksempel ville et lidt mere ekspressivt typesystem have ladet dig opretholde præcis typekontrol uden at skulle duplikere kode.

Et sådant udtryksfuldt system ville tilføje generiske typer til sproget. Generiske typer er typevariabler, der kan instantieres med en passende specifik type for hver forekomst af en klasse. Med henblik på denne artikel vil jeg erklære typevariabler i vinkelparenteser over klasse- eller interface-definitioner. Omfanget af en typevariabel vil derefter bestå af selve definitionen, som den blev erklæret for (ekskl strækker sig klausul). Inden for dette omfang kan du bruge typevariablen hvor som helst, du kan bruge en almindelig type.

For eksempel med generiske typer kan du omskrive din Liste klasse som følger:

abstrakt klasseliste {offentlig abstrakt T accepter (ListVisitor det); } interface ListVisitor {public T _case (Tøm det); offentlig T _case (Ulemper at); } klasse Tom udvider List {public T accept (ListVisitor that) {return that._case (this); }} klasse Ulemper udvider listen {privat T først; privat liste resten; Ulemper (T _forst, List _rest) {first = _first; hvile = _rest; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Nu kan du omskrive AddVisitor at udnytte de generiske typer:

klasse AddVisitor implementerer ListVisitor {privat heltal nul = nyt heltal (0); public Integer _case (Tom at) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Bemærk, at de eksplicitte kaster til Heltal er ikke længere nødvendige. Argumentet at til det andet _sag(...) metoden er erklæret for at være Ulemper, instantere typevariablen for Ulemper klasse med Heltal. Derfor kan den statiske typekontrol bevise det that.first () vil være af typen Heltal og det that.rest () vil være af typen Liste. Lignende instanser ville blive foretaget hver gang en ny forekomst af Tom eller Ulemper erklæres.

I eksemplet ovenfor kunne typevariablerne instantieres med enhver Objekt. Du kan også give en mere specifik øvre grænse til en typevariabel. I sådanne tilfælde kan du specificere denne bundet ved typevariabelens erklæringspunkt med følgende syntaks:

  strækker sig 

For eksempel, hvis du ville have din Listes kun at indeholde Sammenlignelig objekter, kan du definere dine tre klasser som følger:

klasseliste {...} klasse Ulemper {...} klasse Tom {...} 

Selvom tilføjelse af parametriserede typer til Java ville give dig fordelene vist ovenfor, ville det ikke være umagen værd, hvis det betød at ofre kompatibilitet med ældre kode i processen. Heldigvis er et sådant offer ikke nødvendigt. Det er muligt automatisk at oversætte kode, skrevet i en udvidelse af Java, der har generiske typer, til bytekode for den eksisterende JVM. Flere kompilatorer gør det allerede - Pizza- og GJ-kompilatorerne, skrevet af Martin Odersky, er særligt gode eksempler. Pizza var et eksperimentelt sprog, der tilføjede flere nye funktioner til Java, hvoraf nogle blev inkorporeret i Java 1.2; GJ er en efterfølger til Pizza, der kun tilføjer generiske typer. Da dette er den eneste tilføjede funktion, kan GJ-compileren producere bytecode, der fungerer problemfrit med ældre kode. Det kompilerer kilde til bytekode ved hjælp af sletning af typen, som erstatter hver forekomst af hver type variabel med den variable øvre grænse. Det tillader også, at typevariabler deklareres for specifikke metoder snarere end for hele klasser. GJ bruger den samme syntaks til generiske typer, som jeg bruger i denne artikel.

Arbejde der er i gang

På Rice University implementerer den programmeringssprogteknologigruppe, hvor jeg arbejder, en compiler til en opad kompatibel version af GJ, kaldet NextGen. NextGen-sproget blev udviklet i fællesskab af professor Robert Cartwright fra Rices informatikafdeling og Guy Steele fra Sun Microsystems; det tilføjer muligheden for at udføre runtime-kontrol af typevariabler til GJ.

En anden potentiel løsning på dette problem, kaldet PolyJ, blev udviklet på MIT. Det udvides i Cornell. PolyJ bruger en lidt anden syntaks end GJ / NextGen. Det adskiller sig også lidt i brugen af ​​generiske typer. For eksempel understøtter den ikke typeparameterisering af individuelle metoder og understøtter i øjeblikket ikke indre klasser. Men i modsætning til GJ eller NextGen tillader det, at typevariabler kan instantieres med primitive typer. Ligesom NextGen understøtter PolyJ også runtime-operationer på generiske typer.

Sun har udgivet en Java Specification Request (JSR) til at føje generiske typer til sproget. Ikke overraskende er et af de vigtigste mål for enhver indsendelse opretholdelse af kompatibilitet med eksisterende klassebiblioteker. Når generiske typer tilføjes til Java, er det sandsynligt, at et af de forslag, der er diskuteret ovenfor, vil tjene som prototype.

Der er nogle programmører, der er imod at tilføje generiske typer i enhver form, på trods af deres fordele. Jeg vil henvise til to almindelige argumenter for sådanne modstandere som "skabeloner er onde" -argumentet og "det er ikke objektorienteret" -argumentet og adressere hver af dem igen.

Er skabeloner onde?

C ++ bruger skabeloner at give en form for generiske typer. Skabeloner har fået et dårligt ry blandt nogle C ++ - udviklere, fordi deres definitioner ikke er typekontrolleret i parametreret form. I stedet replikeres koden ved hver instantiering, og hver replikering er typekontrolleret separat. Problemet med denne tilgang er, at der måske findes typefejl i den oprindelige kode, der ikke vises i nogen af ​​de indledende instantieringer. Disse fejl kan manifestere sig senere, hvis programrevisioner eller udvidelser introducerer nye instantieringer. Forestil dig frustrationen ved en udvikler, der bruger eksisterende klasser, der typecheck, når de udarbejdes af sig selv, men ikke efter at han tilføjer en ny, helt legitim underklasse! Værre er det, hvis skabelonen ikke kompileres sammen med de nye klasser, vil sådanne fejl ikke blive opdaget, men i stedet ødelægge eksekveringsprogrammet.

På grund af disse problemer rynker nogle mennesker på at bringe skabeloner tilbage og forventer, at ulemperne ved skabeloner i C ++ vil gælde for et generisk system i Java. Denne analogi er vildledende, fordi de semantiske fundamenter for Java og C ++ er radikalt forskellige. C ++ er et usikkert sprog, hvor statisk typekontrol er en heuristisk proces uden matematisk fundament. I modsætning hertil er Java et sikkert sprog, hvor den statiske typekontrol bogstaveligt beviser, at visse fejl ikke kan opstå, når koden udføres. Som et resultat lider C ++ - programmer, der involverer skabeloner, utallige sikkerhedsproblemer, der ikke kan forekomme i Java.

Desuden udfører alle de fremtrædende forslag til en generisk Java eksplicit statisk typekontrol af de parametriserede klasser i stedet for bare at gøre det ved hver instantiering af klassen. Hvis du er bekymret for, at en sådan eksplicit kontrol vil bremse typekontrollen, skal du være sikker på, at det faktisk er det modsatte: da typekontrollen kun foretager et pass over den parametriserede kode i modsætning til et pass for hver instantiering af parametrerede typer fremskyndes typekontrolprocessen. Af disse grunde gælder de mange indvendinger mod C ++ - skabeloner ikke for generiske forslag til Java. Faktisk, hvis du ser ud over, hvad der er meget brugt i branchen, er der mange mindre populære, men meget veldesignede sprog, såsom Objective Caml og Eiffel, der understøtter parametriserede typer til stor fordel.

Er generiske systemsystemer objektorienteret?

Endelig modsætter nogle programmerere sig mod ethvert system af generisk type med den begrundelse, at fordi sådanne systemer oprindeligt blev udviklet til funktionelle sprog, er de ikke objektorienterede. Denne indsigelse er falsk. Generiske typer passer meget naturligt ind i en objektorienteret ramme, som eksemplerne og diskussionen ovenfor viser. Men jeg formoder, at denne indsigelse er rodfæstet i en manglende forståelse af, hvordan man integrerer generiske typer med Java's arvspolymorfisme. Faktisk er sådan integration mulig og er grundlaget for vores implementering af NextGen.