Stil och sånt |
|
Jag hamnar ofta i diskussioner om varför jag tycker att det är
viktigt att man inte blir alldeles för kreativ när man skriver kod
och varför det är viktigt med att hålla koden ren, snygg och
konsistent. Nedan har jag försökt organisera några tankar om detta och
försökt att förklara varför jag tycker som jag gör och organisera
argumenten.
|
TES: Stil och konsistenta idiom i programmering är av central betydelse
för kodens kvalitet.
|
Även om det kan kännas uppenbart vad man menar med stil och idiom kan det
vara på sin plats att förklara vad dessa två begrepp betyder.
Stil är den egenskap av kod som inte påverkar den syntaktiska
tolkningen. En parser bryr sig inte alls om var måsvingarna efter
if-sats sitter, vilka variabelnamn man använder, hur kommentarerna
formatteras, var man lägger in mellanrum, vilket indenteringsdjup
man använder.
Idiom är den samling kodkonstruktioner som man använder
som skiljer sig syntaktiskt, men inte semantiskt.
for (i = 0; i < 17; i++) {
}
gör samma sak som:
i = 0;
while (i < 17) {
i++;
}
Dessa två konstruktioner är idiom för samma sak. De är semantiskt lika, dvs.
de gör samma sak, men de är inte syntaktiskt lika, en kompilator kommer
att bearbeta dem på olika sätt (åtminstone i början).
Givetvis är gränsen mellan idiom och stil ganska flytande. Om man är
petig kan man säga att även identeringsdjupet ändrar hur programmet parsas,
men så länge man inte leker djävulens advokat och sysslar med hårklyverier
borde det vara ganska uppenbart vad som menas.
|
Kreativitetens fingeravtryck |
|
Det är väldigt många människor som kan lyssna på ett kort stycke
musik och snabbt avgöra vem som är kompositör, vem det är som framför
stycket, vem som är dirigent och när inspelningen gjordes. De kan göra
detta utan att någonsin hört just detta stycke, men jämföra små, nästan
ohörbara detaljer med samma detaljer som de har hört tidigare. Egentligen
är två musikstycken samma sak, det är bara lite noter på några linjer och
lite konstiga tecken framför och ibland bakom noterna, ibland tar linjerna
slut och man ritar till några extra linjer om man råkar hamna för långt
ifrån det området som är friskt och sunt (borde man inte ha haft de linjerna
där från början? Vad var det för kommitté som designade notskrift?).
Hur kan ett musikstycke som inte är annat än lite toner och sånt tjafs
avslöja nånting om vem det är som är dirigent eller vem det är som
framför det? Djävulen ligger som vanligt i detaljerna. Genom att vrida
och vända lite på smådetaljer kan man få ett gäng noter att låta helt
olika. Det är det som är stil och samma sak gäller både när man pratar,
skriver, programmerar eller målar bilder.
Häromdagen lyssnade jag på en föreläsning om ett ämne som fascinerar mig
och som jag har studerat själv och ville veta mer om. Föreläsaren var
väldigt kunnig och visste precis vad han pratade om och berättade saker
som jag tidigare inte visste och ville veta mer om. Tyvärr var han också
dödstråkig. Det var nånting med hans tonläge och hela framställning som
skrek: Lyssna inte på mig, det här är tråkigt, ignorera mig,
sov istället.. Jag var tvungen att konsumera större mängder kaffe i
pausen för att inte somna under andra timmen. Samma dag var jag på en
föreläsning i ett ämne som jag redan kan, som jag lika gärna själv kunde
föreläsa om, där det inte sades ett enda ord som jag redan inte kunde
eller visste bättre. Men föreläsningen var ändå spännande och kul
för att föreläsaren kunde framställa det på ett bra sätt. Jag vill inte
spekulera om vad den praktiska skillnaden mellan föreläsningarna var, men
min upplevelse av dem var tvärtom mot vad den borde ha varit om vi bara
tittade på innehållet i dem.
Kod är precis som musikstyckena eller föreläsningarna ovan. Stilen, idiomen
och algoritmerna förmedlar långt mycket mer information än vad kompilatorn
får ur den. En erfaren kodkonnesör kan känna igen enskilda programmerare,
se om det var grupper eller enskilda som skrev ett visst stycke, kan se
var reparationer gjordes, ana till sig den historiska utvecklingen, kan
se om författarna var erfarna eller nybörjare, ibland se att en författare
hade mer erfarenhet av andra språk än det som användes, vilka miljöer
författarna kommer från, etc. På samma sätt kan en implementation av
bubblesort vara rolig och spännande att läsa, medan man behöver träla
sig igenom en malloc med tidskomplexiteten O(1). Visst kan det vara
roligt med lite variation, men skillnaden mellan ett musikstycke och
en bit kod är att musikstycket oftast är till för att underhålla, medan
koden är till för att lösa ett verkligt problem. Underhållningskod
får se ut hur som helst och den dagen ett musikstycke styr min
hjärt- och lungmaskin kommer jag att kräva nanoprecision av dirigenten
och kadaverdisciplin av orkestern. Tills dess kan vi hålla oss till
verkligheten och propagera för kvalitetskod och kreativ musik, inte
tvärtom.
|
TES: Kod är till för att läsas |
|
Ett vanligt missförstånd är att man programmerar för att få ett stycke
kod genom en kompilator och få den att börja exekvera. Det är givetvis
trevligt när ens kod bygger och går att använda, men det borde inte vara
det primära målet. För att förstå vad det primära målet med kod är räcker
det att studera sig själv (om man är programmerare) eller andra programmerare
för att inse vad man spenderar mest tid på. Den hårda insikten blir snabbt
att man faktiskt spenderar mest tid på att läsa koden. För att göra saken
värre, visar det sig att man oftast spenderar mer tid på att läsa andras
kod snarare än sin egen (egen kod vet man ändå hur det är tänkt att den
ska fungera). Skrivning utgör kanske bara någon procent av tiden man lägger
ner på att få ett program klart och körbart. Majoriteten av tiden lägger
man på att leta efter buggar, försöka hitta det stället där nånting magiskt
händer, försöka förstå hur ett API egentligen ska användas, förstå randfall
och andra konstigheter i koden och så vidare. När man till slut har plöjt
igenom ett par romaner kan man sätta sig och klottra ner sin baksidestext.
Ofta går man tillbaka till ett kodstycke efter många år. Ett standardskämt
bland programmerare brukar vara: 'Jag tittade på den där koden och utropade
förskräckt: "Vad var det för idiot som skrev det där!", innan jag insåg
att det var jag som skrev det för ett år sen/för en månad sen/för en
vecka sen/igår.'. Alla erfarna programmerare skrattar hjärtligt åt såna
skämt, men inte för att det ligger någon paradox i dem eller för att det
är särskilt roligt. Man skrattar för att man känner igen sig och är lättad
för att "det var inte jag den här gången". I många fall är det en mardröm
att försöka förstå kod som man skrev bara några veckor tidigare. Helt
vansinnniga konstruktioner blandas med idiotiska algoritmer, med fullständigt
sinnesjuk indentering och cirkusapor som har flyttat runt alla parenteser.
En positiv tolkning av det hela kan vara att det visar att man ständigt
lär sig nya saker och förbättrar sin stil och sina kunskaper. Men hur kan
man veta att den förändring man utsätts för är en förbättring? Skulle
det inte kunna vara så att det är den negativa tolkningen som gäller? Att
man bara fumlar slumpmässigt och springer runt i cirklar utan att egentligen
veta vad man håller på med? Att det enda som gör den gamla koden konstig är
att den inte ser ut som min kod gör ut idag, men inte nödvändigtvis bättre
eller sämre.
Kan det vara så att det enda som gör gammal kod konstig och orsakar
utrop av förskräckelse är att den inte ser ut som idag? Om man visar
ett visst program X (namnet borttaget för att skydda de skyldiga) för
något halvt erfarna C-programmerare blir de förskräckta. Ibland hamnar
man i diskussioner om vilka kodhemskheter man har upplevt och då åker
exemplen fram. "A är ett träsk av spaghettikod och konstiga namn."
"B har ett configure-script som tog över två veckor att köra på
en Sun 3/80" "C består till 80% av olika ifdef", osv. man slänger
hela tiden fram exempel som bara blir värre och värre än det förra,
tills någon plötsligt slänger in konversationsdödaren: "Har någon
tittat på X?". Då blir det plötsligt tyst och många kloka huvuden
nickar och säger: "Jo, X är nog värst.". Det intressanta är att om
man indenterar om X så är koden inte så värst hemsk. Den är inte
speciellt krånglig, den innehåller inte så många galenskaper, inga
bitar i den är fyllda med svart magi som bara författaren kan
begripa. Det enda som egentligen är hemskt med X är att koden är
indenterad baklänges. Den gör precis tvärtemot hur alla andra gör.
Semantiskt är den som vilken annan kod som helst, utseendet gör
dock att den som försöker läsa den i orginalversion helt enkelt
inte kan vrida på sin hjärna och istället för att läsa och tolka
koden ett tecken i taget ger upp och förkastar den som hemsk.
Den går helt enkelt inte att läsa.
samma problem kan uppstå i det vanliga språket när någon får för sig
att slå på kreativiteten och förgylla vår vardag med ett stycke text
som helt saknar interpunktion och versaler ElleR fÖr AtT göRA dEt HelA
ÄnNu MEr SpÄnNaNdE LeKeR MED VeRSAlEr en sån text blir jobbig att läsa
innehållet kan egentligen vara densamma som en helt korrekt text men den
är inte som alla andra det blir konstigt jobbigt oläsligt och man
avvisar snabbt texten som oseriös jag har själv varit i situationer
då jag har avslagit jobbansökningar med ganska imponerade
meritförteckningar eftersom ansökningen var för jobbig att läsa
eftersom författaren var alldeles för kreativ eller ibland inte
tillräckligt kreativ
oFTA SÄGER MAN ATT DET RÄCKER MED ATT VARA KONSISTENT I SIN STIL FÖR ATT
KODEN SKA BLI LÄSBAR, dET ÄR EN HALVSANNING, vISST KAN VI KOMMA ÖVERENS
OM ATT DET ÄR EN GEMEN I BÖRJAN AV MENINGEN. EN PUNKT SKILJER BISATSER.
ETT KOMMA AVSLUTAR MENINGEN OCH SÅ VIDARE, mEN DET BILR FÖRSKRÄCKLIGT
JOBBIGT ATT LÄSA ÄNDÅ, mAN KAN VÄNJA SIG EFTER NÅGRA SIDOR. ELLER KANSKE
NÅGRA MENINGAR. MEN DET UPPKOMMER ETT ANNAT PROBLEM ÄVEN OM MAN VÄNJER
SIG SNABBT, dET FINNS ÄVEN ANDRA TEXTER MAN BEHÖVER LÄSA, vI KAN INTE
LÄRA OSS ATT LÄSA PÅ NYTT FÖR VARJE NY TEXT VI LÄSER. VI MÅSTE KUNNA
ANVÄNDA SAMMA LÄSFÖRMÅGA FÖR FLERA OLIKA TEXTER, iNOM PROGRAMMERINGEN
ÄR DET VÄLDIGT VANLIGT ATT PÅ SAMMA GÅNG LÄSA OCH HOPPA MELLAN KODSTYCKEN
FRÅN FLERA OLIKA KÄLLOR SAMTIDIGT, tESTA ATT HOPPA MELLAN EN TEXT
SKRIVEN PÅ VIT BAKGRUND )ETT PAPPER( OCH EN TEXT MED SVART BAKGRUND
)STÄLL OM SKÄRMEN(. DET BLIR VÄLDIGT ANSTRÄNGANDE FÖR ÖGONEN, lIKA
ANSTRÄNGANDE BLIR DET FÖR HJÄRNAN ATT BYTA KODSTIL HELA TIDEN,
Ibland går det trender i snabbläsning. Det säljs böcker och kurser som
lär människor att läsa snabbt. Det är ganska lätt att lära sig tricket
att läsa en textrad med två ögonkast. Vissa läser hela rader på en gång,
riktiga experter kan läsa en stycke i taget och vissa kan ta in en hel
sida med ett ögonkast. Det intressantaste med snabbläsning är att om man
kontrollerar läsförståelsen visar det sig att de som läser snabbast tar
också in texten bäst. De lär sig helt enkelt att gå direkt från text till
kunskap utan att passera språk/grammatik/interpunktionsfilter. Jag anser
att samma sak gäller kod. Man förstår koden bäst om man kan läsa den snabbt
och inte behöver fördjupa sig i konstigheter och detaljer. Om ett stycke
kod ser ut som "alla andra" slipper man fundera på randvillkor, specialfall,
osv. Man matchar med en blick och går vidare tills man kommer fram till
saker som inte går att greppa med en blick, det är då man antingen
hittar de instressanta bitarna i koden som faktiskt gör nånting (majoriteten
av all kod är bara skelett, byggställning, isoleringsfluff och fasad, de
bitarna som faktiskt gör nånting är få och spridda), eller springer på
buggar. En erfaren programmerare som läser kod som han är van vid
fångar ofta upp buggar genom att se ett stycke kod och mumla "det här ser
skumt ut" innan han egentligen har begripit vad koden gör.
|
| Kodkulturer |
|
Det finns inga enhetliga regler för hur kod skrivs även om vi begränsar
oss till ett enda språk. Vissa nypåfunna språk som Java och Python har
mer eller mindre homogena stilregler, Java för att nästan all exempelkod
följer Suns riktlinjer och programmerare har ännu inte lyckats bli
kreativa och Python eftersom språket kräver en viss sorts indentering av
programmeraren, men även inom dessa kulturer förekommer det rebeller som
antingen bryter mot kutym (inom Java) eller försöker vara så kreativa
som möjligt inom de mer eller mindre strikta reglerna (Python).
Frågan man kan ställa sig är: "Varför?". Det måste väl ändå vara ganska
svårt att gå över från en kodstil till en annan, man måste vänja sig vid
att läsa saker på ett annat sätt, man måste ställa om tempot, man måste
blanda sin kod med annan kod som ser helt annorlunda ut. Ofta är det för
att vara annorlunda. Man vet att man inte kan bidra med nånting nytt,
man vet att man inte är tillräckligt kreativ för att implementera nånting
intressant, man vet att man inte är tillräckligt smart för att uppfinna
några nya trick och algoritmer, men man kan alltid ändra på ytan. I vissa
andra fall är det en markering om att man vill vara annorlunda än alla
andra. I de fallen kan man tala om en viss sorts kultur. Se bara på
större företag. Varje gång ledningen byts ut blir det en omorganisation.
Inte för att det egentligen spelar någon roll, utan för att en
omorganisation visar att man är där och Gör Nånting.
Om man tittar på operativsystemskärnor (specifikt på Unix-liknande
operativsystem), upptäcker man att de liknar varandra. Det är inte bara
för att många av dem delar rötter eller kod, ofta är det så för att en
utvecklare som är upplärd i en miljö och går över till en annan miljö
tar med sig stilen, man kan ju inte lära en gammal hund att sitta. Efter
ett tag har korspollineringen mellan systemen blivit så extensiv att
stilarna är mer eller mindre konsistenta mellan projekt som i princip
inte har nånting med varandra att göra. Stilen i Linux-kärnan är i
princip densamma som den officiella (och väldokumenterade) stilen för
BSD-kärnor, som är i princip samma stil som användes för orginal-Unix.
Men det är inte bara stilen som är lik, många programmeringstrick är
mycket lika varandra utan att vara dokumenterade eller tvingande nånstans.
Ofta beror det på hur själva miljön beter sig. En operativsystemskärna
är för det första en enorm klump kod, alltså vore det dåligt om allting
hette samma sak, väldigt många structar har fält som har prefix med
en eller ett par bokstäver. Jag har aldrig sett något stildokument som
rekommenderar det, men de flesta gör så ändå eftersom de vet konsekvenserna
av att inte göra det. Om man tittar på länkade listor i kärnor brukar
de oftast implementeras av in-line fält i structar och inte externa
cons-celler. Det är inte heller dokumenterat nånstans, men kostnaden och
restriktionerna för malloc i en kärna gör det mycket bekvämare att göra
på ett sätt och inte ett annat. Om man ser på felhantering så är det
väldigt mycket goto som används, mest för att man ofta behöver låsa
och frigöra resurser och vara noga med felhantering, då är det bekvämast
att samla all uppstädning på slutet av en funktion och hoppa dit om
saker gick snett, istället för att upprepa mer eller mindre samma kod
om och om igen för varje fall då nånting kan gå snett.
Om man tittar på kod för stora program med grafiska användargränssnitt
är den också konsistent på något konstigt sätt. Gnome ser ut som KDE
som ser lite ut som OpenOffice som ser lite ut som Mozilla som liknar
exempelkod från Microsoft. Om man jämför Mozilla med en operativsystemskärna
är det inte bara som att läsa litteratur på ett annat språk, men även
från en annan plantet, från ett annat universum och på fel våglängd.
En kärnahacker känner sig handikappad i användargränssnittskod. Inte
bara för att koncepten är okända men också för att det finns en fantastisk
mängd konstig UpperCaseOnani som sticker i ögonen, fullständigt kaos
av funktioner, överlagringar, minnesallokeringar och annat smått och
gott som inte går att begripa. När en användargränssnittsprogrammerare
tittar på kärnakod så blir han på samma sätt förvirrad av att allting
ser likadant ut, allting heter samma sak och koden är platt och
består av enorma funktioner som gör allt.
Om man tittar på all kod som sysslar med 3D-grafik ser man att det handlar
om artister som saknar all disciplin, vett och sans och man förstår direkt
varför alla moderna spel är så buggiga, men det kanske är förtal så jag
låter bli att utveckla den tanken för mycket.
Det kan finnas många förklaringar på hur dessa kulturer uppstår. När det
gäller operativsystemskärnor beror det nog mycket på att alla följer
K&R och det är en ganska begränsad skara elitister och alla som är
annorlunda bli helt enkelt utfrysta. Användargränsnittskulturen kommer
nog från Microsoft och deras stil och alla gymnasiekurser som bygger
på Microsoftgränssnitt. Och så vidare.
|
| Standardkonstruktioner - idiom |
int foo[4711];
for (i = 0; i < sizeof(foo) / sizeof(foo[0]); i++)
blahonga(foo[i]);
|
typedef signed int Integer;
typedef Integer *Index;
typedef Integer MyArray[4711];
#define MyArray2Reference(i) (&i[0])
MyArray foo;
Index foop = MyArray2Reference(foo);
do
blahonga(foop++);
while (foop < &foo[4711]);
|
Dessa två kodbitar gör samma sak (tror jag). Den första begriper
jag direkt. En person som skulle ge mig den andra kodbiten (ja, jag
har sett liknande konstruktioner i verkligheten med typedef-onanin och
macrona gömda bakom fem lager includefiler) skulle antingen åka på
omskolningsläger eller få sparken om han envisades att fortsätta göra
så. Den första ser man direkt hur många gånger loopen kommer att snurra,
vilket element som kommer att vara det sista, även om inte inte känner
till storleken på "foo" vet man att loopen inte kommer att gå utanför
arrayen, etc. Det går inte att se i det andra exemplet. Man kan tänka
sig en programmeringskultur som skulle kunna begripa det andra exemplet
direkt och tycka att for-loopen är konstig, men det är inte min kultur.
Vi läser inte kod genom att läsa varje bokstav i taget och applicera
en strikt parser på den. Vi läser kod genom att göra övergripande
pattern-matching mot tidigare kodstycken som vi har läst. Därför är
det trivialt att hitta buggen i det första kodbiten i följande
exempel, medan det andra är mycket svårare.
int foo[4711];
for (i = 0; i <= sizeof(foo) / sizeof(foo[0]); i++)
foo[i]++;
|
int foo[4711], *foop;
foop = &foo[4711];
again:
(*--foop)++;
if (foop != foo) goto again;
|
Jag skulle kunna erbjuda pengar till den som kan läsa den andra
kodsnutten och på mindre än 30 sekunder säga hur den kommer att göra
fel. Jag tror att jag skulle kunna få behålla mina pengar ändå.
En erfaren programmerare läser en for-loop med en blick och vet exakt
vad den gör, vad randfallen är och hur den kommer att bete sig. Jag
kan tugga mig igenom 20MB diff på någon timme (gjorde det häromdagen)
och hitta fel i den så länge den är i en stil som jag är van vid.
|