- Log ind eller Opret konto for at kommentere
- Anmeld denne kommentar
At Scala har alle disse, bortset fra den sidste, som vel kræver JVM-ændringer
Jeg skal være den første til at indrømme, at der er ting, som kunne have været smartere i Java. Det skulle også være mærkeligt, hvis ikke der i løbet af godt 20 år er fundet på bedre måder at gøre ting på, og som bagudkompatibiliteten (som jeg sætter stor pris på) gør det svært at introducere. Alt taget i betragtning synes jeg dog, at antallet af egentlige smuttere er begrænset, og der bliver rettet op på dem - introduktionen af java.time til erstatning for java.util.Date og venner er et godt eksempel.
Nedenfor er en håndfuld eksempler på ting, som jeg synes er signifikant mere besværlige end nødvendigt. Folks forskellige baggrunde giver anledning til forskellige præferencer, og det er altid lærerigt at få et nærmere indblik i dem, så hvor synes I især, at skoen trykker?
Som jeg ser det kan manglerne ret beset inddeles i to overordnede kategorier. Den ene er konstruktioner, som også findes i andre sprog, men som fylder mere i Java - i vidt omfang det, mange benævner boilerplate-kode.
Det primære ankepunkt her er nok de basale (bean)klassekonstruktioner, altså settere og gettere, equals og hashCode. De generer mig som nævnt i et tidligere indlæg ikke så meget, fordi jeg ikke bruger meget tid på dem. Ja, de fylder i koden, men man kan i vidt omfang se bort fra dem, og bliver autogenereret af IDE'et. Det hænder endda, at de kan bruges til noget - gettere, som transformerer data, eller bare muligheden for at sætte breakpoints.
private static final Set<String> KEYS = ("foo", "bar", "baz");
public (int, double) compute() { ... return (x, y); } (int a, double b) = compute();
Det er især constructors, hvor defaultværdier ville give mening. Constructor chaining er fint nok, men så snart der kommer meget mere end to constructors i samme klasse taber man overblikket. Bare det faktum at det bliver meget langt med et hypotetisk kodeeksempel illustrerer fint pointen :-)
Bortset fra i løkker er det reglen snarere end undtagelsen, at variable er konstante. Det ville forhindre fejl og spare tid, hvis man eksplicit skulle erklære dem som ikke-konstante.
Den anden kategori er ting, som det aktuelt ikke umiddelbart kan lade sig gøre at lave i Java, for eksempel
Man kan i mange tilfælde lave en billig udgave i Java ved at definere felter og metoder i en enum, og hvis det går helt galt er der den verbose løsning med en subclass pr. element i typen.
Flaskehals nummer et i moderne computere er hukommelsen, så hvis man kan undgå at skulle følge en pointer er det en god ting. Der er derfor i nogen tilfælde signifikant overhead forbundet med at alting (bortset fra værdier af primitive typer) tilgås gennem referencer - det er igen ikke noget, som giver mig søvnløse nætter, men i HPC-agtige applikationer er det centralt. JEP 169 arbejder i øvrigt på at få det ind i Java, så der er lys forude.
(*): Jeg burde være fan af et programmeringssprog, hvis navn er mine initialer. Eller omvendt.
At Scala har alle disse, bortset fra den sidste, som vel kræver JVM-ændringer
Scala har tupler men det er faktisk lidt af en unode at benytte dem til returværdier. I stedet bør man bruge en case class til formålet.
case class NavnAdresse(navn: String, adresse: String) def findById(id: Int): NavnAdresse = ???
i stedet for:
def findById(id: Int): (String, String) = ???
Og hermed endnu en pointe - der er intet der stopper dig i at implementere den første findById i Java, men det er så bøvlet at man i stedet ser folk returnere Object[] og andre horrible hacks.
Burde være obligatorisk på metoder som derefererer objekt referencer, som kunne være null.
Omvendt burde Object a aldrig kunne være null, medmindre den var erklæret som f.eks Object* a
Burde være obligatorisk på metoder som derefererer objekt referencer, som kunne være null.
Kast et blik på Kotlin (disclaimer: jeg har kun læst om sproget)
Mikkel, har du overvejet at benytte Groovy, evt. med statisk kompilering og typecheck? Stort set alle dine ankepunkter er netop forbedret i Groovy. Og så er Groovy jo Java, og kan mix-kompiles, så man kan flytte lidt ad gangen.
Jeg synes nu ikke Java 8 er så ringe endda. Stream API, functional interfaces, lambdas osv. er ganske glimrende udvidelser til sproget. Ligesom default methods på interfaces (traits i Groovy er dog bedre IMHO).
Jeg husker ikke hvem, men en eller anden har en gang sagt "Over time, every programming language evolves to look more and more like ML".
Det er ikke helt ved siden af: Tupler, closures, og i mindre grad typeinferens og algebraiske typer er efterhånden blevet tilføjet mange mainstream programmeringssprog, der ikke er født med disse, og ideen om, at pointere per default ikke kan være null er også ved at vinde frem.
Jeg er ikke den store Java-bruger, men jeg synes, at der udover Mikkels ønsker er nogle oplagte ting, man med fordel kunne tilføje:
Du mener vel lisp?
"Greenspun's Tenth Rule of Programming: any sufficiently complicated C or Fortran program contains an ad hoc informally-specified bug-ridden slow implementation of half of Common Lisp."
Philip Greenspun
"We were not out to win over the Lisp programmers; we were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp."
Guy Steele, Java spec co-author
Begge fra: http://www.paulgraham.com/quotes.html
At Scala har alle disse, bortset fra den sidste, som vel kræver JVM-ændringer
Nu kan forfatteren jo godt semi-kolonner, så du får ham nok ikke over på Scala :)
"Det primære ankepunkt her er nok de basale (bean)klassekonstruktioner, altså settere og gettere, equals og hashCode. De generer mig som nævnt i et tidligere indlæg ikke så meget, fordi jeg ikke bruger meget tid på dem. Ja, de fylder i koden, men man kan i vidt omfang se bort fra dem, og bliver autogenereret af IDE'et."
Selv om de ikke generer dig så er der en meget effektiv måde slippe for dem. Jeg har bruge lombok (https://projectlombok.org/) i årevis og savner ikke at skrive/læse gettere, settere, equals, hashCode og toString(). Du skrive bare @Getter, @Setter eller måske @Data på din klasse og så er det klaret. Det er nydeligt integreret i IntelliJ og sikkert også andre IDE'er.
Nu kan forfatteren jo godt semi-kolonner, så du får ham nok ikke over på Scala :)
Scala har valgfrie semikolonner, så han kan bare implementere en precommit-hook, der fejler når man glemmer dem, så ved udviklerne også hvem der bestemmer ;)
Tilbage i 90erne tilvalgte og fravalgte Gosling+Naughton features, ud fra hvad de ønskede at sproget skulle bruges til. Som i alle sprog, så når man koder, savner man nogle gange features som findes i andre sprog. F.eks.: - funktionspointere - duck-typing - operator overloading - multipel nedarvning - procedural kode - comesfrom Når man så støder ind i et problem, hvor man savner en feature,så er det, at mængden af boilerplatekode stiger. Det må man leve med, eller skifte sprog.
Når man så støder ind i et problem, hvor man savner en feature,så er det, at mængden af boilerplatekode stiger
Et ordentlig programmeringssprog bør kunne udvides med den pågældende feature, med et minimum af arbejde, så man som anvender ikke skal interface, subclasse, typecaste, hækle, strikke, klippe hækken, vaske bilen og lufte hunden, før man kan løse sin opgave :)
Jeg kan tage fejl, men i min optik er boilerplate en masse besværgelser man skal igennem før man (endelig) kommer til at kunne løse sin opgave.
I denne forbindelse kan jeg ikke lade være med at linke til denne ganske humoristiske artikel om Java Frameworks: http://discuss.joelonsoftware.com/default.asp?joel.3.219431.12&cm_mc_uid...
No offence - respekt for de der mestre deres værktøj: Java, Pascal, ML, C, ...
"Over time, every programming language evolves to look more and more like ML"
Jeg har hørt den udtalt af Bob Harper, men jeg ved ikke om han var den første. Udsagnet passer iøvrigt også fint for Common Lisp. Det var bare en milepæl på vejen mod Standard ML. Vi er nu forbi Common Lisp-positionen og på vej videre da sprog nu efterhånden får algebraiske datatyper og pattern match :)
Iøvrigt er algebraiske datatyper's værd i høj grad bestemt at opgaven og om du har dem til rådighed. Deres styrke er at de er i stand til at eliminere en masse bolsk blindhed fra din kode (boolean blindness), hvilket lader dig skrive langt mere præcise programmer, hvor scope styrer hvad du kan gøre med dine data. Det giver en udviklingsform der af Yaron Minsky er beskrevet som "make illegal representations impossible". Altså, lav dit program sådan at de eneste måder du kan konstruere data på er de for programmet lovlige. Resultatet er at du kan eliminere en hulens mængde defensiv kode og checks.
Jeg kan tage fejl, men i min optik er boilerplate en masse besværgelser man skal igennem før man (endelig) kommer til at kunne løse sin opgave.
Jeg brugte begrebet "boilerplate" lidt upræcist. Det var nærmere: håndkodning af den feature, som har brug for. F.eks. hvis man sidder med C og virkelig kunne bruge lidt klasser med nedarvning og virtuelle funktioner, så kan man kode det selv, men sjovt er det ikke. Eller hvis man sidder med Java og virkelig kunne bruge multipel nedarvning, så kan håndkode det selv, men specielt kønt er det ikke. Eller hvis man sidder med et shellscript og virkelige godt kunne bruge floating-point aritmetik, så kan man humpe det igennem med brug af 'bc'. Osv.
Udfordringen er at når man har et problemdomæne, hvor de fleste delproblemer kan løses elegant vha. sprogets faciliteter, men der er et eller flere delproblemer, hvor mængden af "udenomskode", for at få problemet løst, er høj. Det kan være mindre detaljer, så som at lave 64-bit unsigned udregninger i Java; eller større ting, så som at snakke med en webserver i C. I begge tilfælde ender man med at skrive mere ulegant kode end hvis man brugte et andet sprog.
Det giver en udviklingsform der af Yaron Minsky er beskrevet som "make illegal representations impossible". Altså, lav dit program sådan at de eneste måder du kan konstruere data på er de for programmet lovlige.
Mener du ikke "Make illegal states unrepresentable"?
Og det kræver vel nærmest dependent types eller i det mindste GADT.
@Ivan: Så forstår jeg bedre. De problemer du nævner har lidt forskellig skala gående fra at det kan 'sagtens' løses med kald af en biblioteksfunktion skrevet i et andet sprog til at man har det forkerte værktøj til opgaven. I det sidste tilfælde skal man så overveje om man kan omformulerer problemet til det værktøj man har. F.eks. klasser - hvis ønsket bare er struktur, kan man gøre det på anden vis, hvis det er fordi kunden ønsker en objektmodel i koden må man se om man kan sno sig igennem alligevel - ellers må man jo gribe ud efter et OO sprog :)
Mener du ikke "Make illegal states unrepresentable"?
Og det kræver vel nærmest dependent types eller i det mindste GADT.
Det kræver dependent types og en masse bevismaskineri, men det er jo ikke en alt-eller-intet ting: Jo flere invarianter du kan garantere via typesystemet, des bedre, også selvom der stadigvæk kan repræsenteres ugyldige tilstande. Jeg synes sprog som SML og Haskell har en god balance mellem konceptuelt/notationelt overhead og statiske garantier. Der findes sprog med meget stærkere typesystemer - Agda, Coq og Idris har jeg selv prøvet - men man ender ofte med at bruge enormt meget tid på at føre korrekthedsbeviser for relativt trivielle ting. At finde balancen i et typesystem er i virkeligheden mere et spørgsmål om brugergrænsefladedesign, end om matematisk logik.
Tilbage i 90erne tilvalgte og fravalgte Gosling+Naughton features, ud fra hvad de ønskede at sproget skulle bruges til.
Spørgsmålet er så, hvad de havde tænkt sig at Java skulle bruges til. Man kunne argumentere for, at de allerede i 1995 fandtes bedre sprog til de fleste typer opgaver.
Spørgsmålet er så, hvad de havde tænkt sig at Java skulle bruges til.
Sproget var oprindeligt designet til interaktivt TV, altså det vi i dag kalder "Smart TV", men det var for omfattende til at kunne bruges til det formål i starten af 1990'erne. Ifølge Wikipedia var hoveddesignprincipperne:
There were five primary goals in the creation of the Java language:
- It must be `"simple, object-oriented, and familiar".
- It must be "robust and secure".
- It must be "architecture-neutral and portable".
- It must execute with "high performance".
- It must be "interpreted, threaded, and dynamic".
I Java 9 er der tilføjet funktionalitet, der gør det muligt at instanstiere immutable collections og maps med få elementer ved hjælp af simple metoder, se JEP 269: Convenience Factory Methods for Collections.
Dit Set-eksempel vil komme til at se nogenlunde sådan her ud:
-> Set<String> set = Set.of("foo","bar","baz") | Added variable set of type Set<String> with initial value [foo, bar, baz] -> set.add("bazz") | java.lang.UnsupportedOperationException thrown | at Collections$UnmodifiableCollection.add (Collections.java:1056) | at (#22:1)
Formålet med tilføjelsen er, at lette udviklerens tilværelse en smule uden at ændre implementeringen af sproget med den fulde funktionalitet af Collection Literals. I JEP 269 bliver det yderligere begrundet, hvorfor sprogdesignerne har fravalgt Collection Literals i den næste version af Java.
There were five primary goals in the creation of the Java language:
- It must be "simple, object-oriented, and familiar".
- It must be "robust and secure".
- It must be "architecture-neutral and portable".
- It must execute with "high performance".
- It must be "interpreted, threaded, and dynamic".
Tak for googling, Torben :)
Jeg synes det er sigende, at disse principper primært omhandler platformen JVM. Rent sprogdesign-mæssigt er det reelt kun punkt 1, der er rigtigt relevant. Og det kogte man så ned til "C++ er velkendt og objektorienteret, hvis nu vi fjerner rigtigt grimme ting, indtil det er simpelt, er der nok ikke nogen, der opdager, at det der er tilbage, heller ikke er ret kønt..."
eg synes det er sigende, at disse principper primært omhandler platformen JVM. Rent sprogdesign-mæssigt er det reelt kun punkt 1, der er rigtigt relevant.
Sikkert, trådet og dynamisk er vel også sproglige features, selv om de påvirker designet af den underliggende platform. Robust til nød, da f.eks. "Det skal ikke være muligt at følge referencer til lager, der ikke er allokeret og initialiseret" handler om robusthed, men skal reflekteres i sprogdesignet. Men jeg giver dig ret i, at det ikke giver mening at designe et sprog til at være effektivt eller fortolket. Selv meget dynamiske sprog kan implementeres med oversættelse i stedet for fortolkning, selv om fortolkning klart er den nemmeste model for meget dynamiske sprog.
Det er i øvrigt sigende, at selv om JVM var designet til fortolkning, så bruger de fleste moderne implementeringer oversættelse.
Og det kogte man så ned til "C++ er velkendt og objektorienteret, hvis nu vi fjerner rigtigt grimme ting, indtil det er simpelt, er der nok ikke nogen, der opdager, at det der er tilbage, heller ikke er ret kønt..."
Jeg vil ikke umiddelbart sige at Java er ukøn, men de fravalgte features, som fandtes i andre sprog på det tidspunkt. Nogle af dem er senere blevet tilvalgt, f.eks. generics/templates, reflection, private strukturer/inner classes.
Mængden af features i et sprog er endeligt, mens mængden af potentielle men ikke understøttede features er uendelig, så der vil altid være mangler i et sprog. Spørgsmålet er hvad man en gang imellem savner, og er bøvlet at kode selv. For Java vil jeg sige: multipel nedarvning og preprocessor. Begge features kan misbruges,men de kan også bruges effektivt.
Noget som jeg generelt savner i sprog (undtagen yacc) er kompakt og letlæselig implementation af tilstandsmaskiner. Det kunne da være fedt at blot kunne skrive noget i stil med dette for en kaffemaskinekontroller:
fsa kaffemaskine { off (on) -> preheating //tænd varmelegemet //tænd 'on' lysdioden preheating (goal-temperature-reached) -> ready //tænd 'klar' lysdioden ready (brew) -> brewing //åbn for vandet //tænd timer ready (off) -> off //sluk varmelegemet //sluk 'klar' lysdioden brewing (off) -> off //sluk for vandet //sluk for varmelegemet //skyl restvandet ud //sluk 'klar' lysdioden //sluk 'on' lysdioden ...
og så blot:
if(gpio[7]) kaffemaskine.handle_event(on)
Se dét er en feature jeg gerne så i C/C++/Java/Python/COBOL/shell/... Ja, man kan sagtens implementere det selv i ovennævnte sprog, eller bruge biblioteker til det, men jeg har aldrig set det direkte understøttet i et sprog (undtagen til dels yacc, men det vil måske være at voldtage det).
Mener du ikke "Make illegal states unrepresentable"?
Og det kræver vel nærmest dependent types eller i det mindste GADT.
Ja, du har ret i den citation.
Som Troels skriver, så kan mange illegale tilstande godt elimineres via et typesystem der er stærkere end det Java har, uden at du kommer over i dependent types (eller GADTs). Så det kræver ikke nødvendigvis at du har adgang til et tungt typemaskineri, bare et der er marginalt stærkere end det Java tilbyder. I MLs tilfælde er systemet endda også simplere i sit grundregelsæt, men sproget er mere abstrakt.
Noget som jeg generelt savner i sprog (undtagen yacc) er kompakt og letlæselig implementation af tilstandsmaskiner.
Det er da nemt nok i mange sprog?
// Scala object Kaffemaskine { trait Event case object On extends Event case object GoalTemperatureReached extends Event case object Brew extends Event case object Off extends Event trait State case object OffState extends State case object Preheating extends State case object Ready extends State case object Brewing extends State var state: State = OffState def handleEvent(event: Event): Unit = state = (state,event) match { case (OffState,On) => Preheating case (Preheating,GoalTemperatureReached) => Ready case (Ready,Brew) => Brewing case (Ready,Off) => OffState case (Brewing,Off) => OffState } } }
ML og Haskell gør det naturligvis endnu mere koncist og elegant.
Sikkert, trådet og dynamisk er vel også sproglige features
Jo, du kan vel sige at pointere er udeladt fra sproget af sikkerhedsårsager.
Mig bekendt er den eneste understøttelse for tråde på sprogniveau i Java synchronized nøgleordet. Det er lidt syntaktisk sukker for at gå ind og ud af en monitor, med en finally block. I øvrigt inviterer det til deadlocks fordi monitoren oftest er objektet selv, hvilket gør det umuligt at kontrollere, hvem der ellers låser på det.
Jeg ved ikke hvad der præcist mentes med dynamic, det er jo et temmeligt overloaded begreb, men eftersom Javas typesystem er statisk, antog jeg at der refereres til en egenskab ved JVM.
Det er da nemt nok i mange sprog?
[snip: pæn Scala implementering af en tilstandsmaskine]
Der er forskel mellem at let kunne implementere tilstandsmaskiner (med patternmatching i dit eksempel), og at sproget direkte understøtter tilstandsmaskiner. Hvis sproget understøtter det direkte, så vil jeg forvente at oversætteren kan advare om: - manglende transitioner som gør at tilstandsgrafen er opdelt - start- og sluttilstande, som ikke er markeret som sådan
Hvis jeg skal være lidt brutal, så vil jeg sige at i dit eksempel så er alt op til nøgleordet "match" boilerplate (nødvendigt fordi Scala er statisk typet), og det kunne skrives mere kompakt i shellscript:
function handle_event() { case ${1}-${2} in off-on) echo "preheating" ;; preheating-goaltemperaturereached) echo "ready" ;; ready-brew) echo "brewing" ;; ready-off) echo "off" ;; brewing-off) echo "off" ;; esac }
som også implementerer tilstandsmaskinen vha. patternmatching.
Men jeg tror at vi er nået frem til en feature,somJava (og C/C++/cobol/...) kunne bruge, som vil gøre nogle problemer lettere og mere direkte at formulere: patternmatching
manglende transitioner som gør at tilstandsgrafen er opdelt - start- og sluttilstande, som ikke er markeret som sådan
Rent faktisk vil mit program netop give en fejl ved oversættelse fordi der mangler tilstande. Hvis programmet skal være korrekt, skal du enten tilføje de manglende "case" eller lave en wildcard "case _ => foo". Du kan også gøre det delvist med "case ( _ ,Off) => OffState" eller "case (OffState, _ ) => OffState // do nothing". Du kan endvidere gruppere ting ved at lave din states i et nedarvnings hierarki eller ved at implementere flere traits.
Scala fylder ret meget når der skal defineres simple datatyper. I Standard ML og Haskell er det en one liner at definere alle tilstandende. Ja selv Java er faktisk bedre på lige det punkt, idet man kan definere enums på en koncis måde.
Standard ML:
datatype state = OffState | Preheating | Ready | Brewing
Men altså, det er ikke boilerplate netop fordi det er nødvendigt for at oversætteren kan gøre opmærksom på fejl såsom manglende transitioner.
Men jeg tror at vi er nået frem til en feature,somJava (og C/C++/cobol/...) kunne bruge, som vil gøre nogle problemer lettere og mere direkte at formulere: patternmatching
Matching er en eliminationsform i logik. Du skal derfor formentlig også indføre konstruktionsformen som du kan eliminere. Det er de føromtalte algebraiske datatyper. Ellers har du sådan set ikke et konsistent regelsæt. Den klassiske "switch" i C er rimeligt begrænset og en af grundende er at den ikke følges af en tilsvarende form.
Hvis du kigger på transitionsformer og lovlighed, så læner du dig rigtigt hurtigt op af en model-checker for at kunne afgøre om transitionerne er lovlige. Det er interessant, men jeg kender ikke noget mainstreamsprog der udnytter disse ideer. Men er du villig til at approximere lovligheden, så har du masser af muligheder i randomiseret testing, og det er et felt jeg har beskæftiget mig rigtigt meget med i praktiske situationer.
Hvis du kigger på transitionsformer og lovlighed, så læner du dig rigtigt hurtigt op af en model-checker for at kunne afgøre om transitionerne er lovlige. Det er interessant, men jeg kender ikke noget mainstreamsprog der udnytter disse ideer.
Nej, og det er en skam. På laveste niveau er alt bits, bytes, registre, adressegymnastik, og betingede hop. Et programmeringssprog er at tilbyde abstraktioner. 50-60 års erfaring har vist at jo tidligere fejl findes jo billigere er de at rette. Så min holdning er at kompilere skal finde alle de fejl de overhovedet kan i brug af abstraktionerne som de tilbyder. De fleste kompilere vil detekte:
int x = 1/0;
og nægte at oversætte. Nogle vil detekte:
int a[20]; a[42] = 17;
men hovedparten vil blot advare og lystigt fortsætte med oversættelsen. Jeg synes at det er dumt, for programmet er ikke gyldigt. For at kompilere kan detekte fejl, så kræver det at sproget hæver sit niveau over bits og bytes, for at ikke blot hævet programmøres produktivitet direkte, men også for at kunne finde ugyldig og tvivlsom brug på oversættelsestidspunktet. Og nogle gange fravælge understøttelse af lavniveauskonstruktioner. F.eks. understøttter java ikke goto, men tilbyder højere abstraktioner (do,while, for, labeled-break). Hvad med f.eks. trådsynkronisering? Et sprog som blot siger "du kan bare selv implementere Dekkes algoritme" lyder dumt. Java tilbyder 'synchronized', men måske var et højere niveau bedre? Hvad med f.eks. Adas rendevouz eller Erlangs message-passing? Måske var de bedre end det, somJava tilbyder nu.
At overlade abstaktioner til libraries og frameworks udskyder en del af fejldetekteringen til kørselstidspunkten, hvilket gør fejlfinding dyrere. På den anden side holder det også sproget lille og lettere at implementere.