Programmerings­språk klarer ikke 0,1 + 0,2: - Merkelig at vi fortsatt har det sånn

- På tide at språkene våre får støtte for riktige desimaltall, synes Filip Van Laenen.

Filip Van Laenen synes det er snodig at vi fortsatt sliter med desimaltall i dagens programmeringsspråk. 📸: Privat
Filip Van Laenen synes det er snodig at vi fortsatt sliter med desimaltall i dagens programmeringsspråk. 📸: Privat Vis mer

Hvor mye er 0,1 + 0,2? Det er lett, for det er 0,3, har du lært på barneskole. Men overraskende nok er dette et regnestykke som mange programmeringsspråk ikke helt klarer.

Datamaskiner er flinke med tall, og de kan regne mye raskere og med høyere presisjon enn mennesker. Det er i utgangspunktet riktig, men med noen viktige nyanser.

Innenfor bruksområdet for tallsystemet de bruker er det helt klart riktig, og som regel snakker vi da om det binære tallsystemet. Men vi mennesker er vant til å bruke det desimale tallsystemet, og det fører fort til en del små misforståelser mellom menneske og maskin.

Er 0.1 + 0.2 > 0.3 ?

For å vise hva som kan skje når man regner internt i det binære tallsystemet, men presenterer både input og output i det desimale tallsystemet, kan vi bruke en REPL. REPL står for Read-Evaluate-Print-Loop, og det er et lite verktøy man kan bruke for å teste ut små ting i et programmeringsspråk.

De fleste programmeringsspråk har en eller annen REPL som man kan kjøre fra kommandolinjen, i tillegg til at det fleste IDE-er tilbyr lignende funksjonalitet.

Ta for eksempel JShell (for Java) eller IRB (for Ruby), og prøv ut regnestykket fra innledningen. Når jeg ber dem å evaluere 0.1 + 0.2, gir begge to samme svar: «0.30000000000000004».

Det skal sies at feilmarginen ikke er særlig stor, men konsekvensen av dette er at i Java, så er svaret ja: 0.1 + 0.2 > 0.3.

Løsningen

Lager man funksjonalitet som bygger på flytende tall, fører det fort til et par utfordringer. Trenger du om av en eller annen grunn en funksjon som summerer to tall uten videre, vil enhetstesten assertEquals(0.3, sum(0.1, 0.2)) feile.

Løsningen er da ikke å endre det til assertEquals(0.30000000000000004, sum(0.1, 0.2)), men å bruke assertEquals(0.3, sum(0.1, 0.2), 1E-10). Det siste argumentet, 1E-10, er deltaen man tillater, det vil si feilmarginen som man kan leve med i enhetstestene.

Forresten byr Java (og de fleste andre programmeringsspråk som har to typer flytende tall) på en liten overraskelse på toppen av den første.

For selv om 0.1 + 0.2 == 0.3 resulterer i false, så er 0.1F + 0.2F == 0.3F likevel true. Som default tolkes desimaltall å være doubles, d.v.s. flytende tall med dobbel presisjon (64-bit IEEE 754 floating point). Men bruker man floats, d.v.s. 32-bit IEEE 754 floating points, så treffer man i dette tilfellet det forventede resultatet likevel.

«Forresten byr Java (og de fleste andre programmeringsspråk som har to typer flytende tall) på en liten overraskelse på toppen av den første.»

Spise vår egen DDD-medisin

Det er egentlig merkelig at vi i år 2022 fortsatt lever med at 0.1 + 0.2 > 0.3 når vi skriver kode.

Snakker vi med kunder eller brukere er vi som utviklere ofte opptatt av DDD, og at de riktige begrepene brukes i koden vår. Men samtidig programmerer vi i et språk som ikke engang klarer å håndtere enkle regnestykker fra barneskolen riktig.

Det grunnleggende problemet er selvfølgelig at når vi skriver 0.1 i koden vår, eller mater det inn i vår forretningslogikk gjennom et inputfelt, så blir det ikke lagret som 0,1, men oversatt til et binærtall som har verdi 0,100000001490116119384765625. Og da starter jo problemene.

Kanskje er det på tide at vi tar oss sammen, og spiser vår egen medisin? Når vi skriver 0.1 i koden vår, eller lar en bruker taste inn 0,1 i et inputfelt, så bør vi kanskje begynne å håndtere det som et desimaltall?

Det som skjer nå er at vi lurer oss selv og/eller en bruker når vi konverterer et til et binærtall og later som om det fortsatt er det samme. Så kan det hende at vi i forretningslogikken kommer bort i noen beregninger som tilsier at vi trygt kan konvertere til binærtall fordi vi har en begrenset presisjon uansett. Men når vi plusser to tall som har en grei representasjon som desimaltall, så burde vi kanskje la være å ta turen innom binærtall.

Hva mener vi egentlig når vi skriver «0,1»?

Så er spørsmålet kanskje like mye hva vi egentlig mener når vi skriver «0,1». Er det virkelig det matematiske 1/10, eller er det en måling fra en termometer eller en målestokk eller noe annet?

Er det siste tilfellet, så snakker vi jo egentlig om 0,1 ± 0,05, og da burde vi også kunne leve med at 0,1 + 0,2 ikke er helt lik 0,3 når vi kjører en beregning. Selv enhetstesten fra over, assertEquals(0.3, sum(0.1, 0.2), 1E-10), virker da i overkant streng.

Men likevel, jeg synes det er på tide at programmeringsspråk har støtte for riktige desimaltall. Og ikke bare gjennom et bibliotek, men ordentlig innebygget i standarden.

Kunne vi se bort fra bakoverkompatibilitet, ville jeg sagt at uttrykk som 0.1 og 0.2 burde bli tolket som rene desimaltall, og ikke som binære tall med dobbel presisjon. Er jeg i en kontekst hvor det er greit med små avrundingsfeil, klarer jeg fint å skrive 0.1F eller 0.2D. Men for etablerte språk vil en slik endring sannsynligvis skape flere problemer enn det løser.

Inntil videre må vi som utviklere nok leve med at mange desimaltall i programmeringskoden vår ikke er det de gir seg ut for å være. Men hvis det kan være en liten trøst: jobber du med et domene der du ofte har bruk for slike desimaltall og gjør beregninger med dem, så blir du nok minnet på det ganske mange ganger om dagen.