Antra

Sprache

Zahlen im Detail

Antra rechnet so genau, wie es geht, und sagt dir, wenn es nicht mehr geht. Ganzzahlen und Brüche sind exakt und haben keine Größengrenze. Ungenau wird es nur, wenn eine Gleitkommazahl ins Spiel kommt, und das passiert nicht versehentlich: Eine Gleitkommazahl entsteht nur aus Funktionen wie Math.sqrt, aus einer Wurzel wie 2 ** 0.5 oder durch ein ausdrückliches float(...).

Dieses Kapitel beschreibt, welche Zahlen es gibt, wie sie entstehen, was beim Rechnen herauskommt und wo die Grenzen liegen. Als Typannotationen heißen die drei Arten Int, Rat und Float, alle zusammen Number (Kapitel 16).

Drei Arten von Zahlen#

ArtPrädikatBeispielEigenschaft
Ganzzahl (Int)int?42, -7, 2 ** 100exakt, beliebig groß
Bruch (Rat)rat?1/3, 0.5, 4 / 2exakt, Zähler und Nenner beliebig groß
Gleitkommazahl (Float)float?Math.sqrt(2)ungenau, etwa 15 gültige Stellen

Jede Zahl gehört zu genau einer der drei Arten. number? ist für alle drei wahr, und type(...) liefert für alle drei :number.

import Antra::Math

println(int?(42))             # true
println(rat?(1/3))            # true
println(float?(Math.sqrt(2))) # true
println(number?(0.5))         # true

Woher eine Zahl ihre Art hat#

Die Art einer Zahl hängt davon ab, wie sie entsteht, nicht von ihrem Wert. 4 / 2 hat den Wert 2, ist aber ein Bruch, weil eine Division einen Bruch liefert. Math.sqrt(4) hat ebenfalls den Wert 2, ist aber eine Gleitkommazahl, weil Math.sqrt Gleitkommazahlen liefert.

import Antra::Math

println(rat?(4 / 2))           # true
println(int?(4 / 2))           # false
println(float?(Math.sqrt(4)))  # true
println(int?(Math.sqrt(4)))    # false

So weißt du beim Lesen des Codes, was herauskommt, ohne den Wert zu kennen.

Literale#

SchreibweiseArt
42, -7, 10_000Int
0xFF, 0o755, 0b1010Int (auch über 64 Bit: 0xFFFFFFFFFFFFFFFFFF)
beliebig lange Ziffernfolge, z. B. 123456789012345678901234567890Int
0.5, 1_000.25, 3.14Rat (exakt, kein Float!)
1e3, 1e300, 6.02e23Int (1e300 ist eine 1 mit 300 Nullen)
1.5e-3, 1e-20Rat (3/2000, 1/100000000000000000000)

Dezimalzahlen sind in Antra also Brüche: 0.1 ist genau ein Zehntel, auch mit beliebig vielen Stellen. Eine Exponent-Zahl ist ein Int, wenn ihr Wert ganz ist, sonst ein Bruch. Ein Literal darf höchstens 10 000 Stellen ergeben, 1e20000 ist ein Fehler; als Rechnung 10 ** 20000 geht es.

Text, den num(...) liest, folgt denselben Regeln: num("2.5") ist ein Bruch, num("12") ein Int und num("1e30") ein exakter Int; auch num("1_000"), num("0xFF"), num("0o755") und num("0b1010") gehen.

Funktionen#

Funktionen, deren Ergebnis im Allgemeinen nicht exakt darstellbar ist, liefern eine Gleitkommazahl, auch wenn das Ergebnis zufällig glatt ist: Math.sqrt, Math.sin, Math.log, Math.exp und ihre Verwandten. float(...) macht aus jeder Zahl ausdrücklich eine Gleitkommazahl. Dasselbe gilt für ** mit einem Bruch als Exponenten und für Dezimalzahlen, die JSON.parse liest.

Was beim Rechnen herauskommt#

RechnungErgebnis
Int + - * IntInt
Int / IntRat, auch wenn die Division aufgeht: 4 / 2
Rat mit Int oder Rat (+ - * /)Rat
irgendetwas mit einer FloatFloat
Int ** Int (Exponent ≥ 0)Int
Int oder Rat ** Int (auch negativ)Rat: (1/2) ** 2 ist 1/4, 2 ** -1 ist 1/2
** mit einem Bruch oder einer Float als ExponentFloat: 2 ** 0.5 ist die Wurzel aus 2
%die Art wie bei + - *; Vorzeichen wie der rechte Operand
println(7 / 3)        # 7/3
println(7 / 2)        # 3.5 (ein Bruch, als Dezimalzahl angezeigt)
println(1/3 + 1/6)    # 0.5
println(2 ** 100)     # 1267650600228229401496703205376
println(2 ** -1)      # 0.5 (ein Bruch)
println(2 ** 0.5)     # 1.4142135623731 (Float)
println(-7 % 3)       # 2
println(7 % -3)       # -2
println(7.5 % 2)      # 1.5 (ein Bruch, exakt)
println(-7.5 % 2)     # 0.5

a % b ist der Rest a - b * Math.floor(a / b): er hat immer das Vorzeichen von b (wie in Python, Ruby und Clojure, anders als % in C oder Java). Mit Brüchen rechnet % exakt, mit einer Gleitkommazahl als Float; nichts wird vorher auf eine ganze Zahl abgeschnitten.

Eine Gleitkommazahl „steckt an“: Sobald eine im Spiel ist, ist das Ergebnis wieder eine. Umgekehrt wird aus exakten Zahlen nie still eine ungenaue.

Teilen durch null ist immer ein Fehler, auch bei Gleitkommazahlen: x / 0, x % 0 und 0 ** -1 (ebenso float(0) ** -1).

Exakt und unbegrenzt#

Ganzzahlen haben keine Obergrenze. Wird ein Ergebnis größer als 64 Bit, rechnet Antra einfach weiter:

println(9223372036854775807 + 1)   # 9223372036854775808
println(2 ** 100 + 1)              # 1267650600228229401496703205377

Dasselbe gilt für Brüche. Zähler und Nenner dürfen beliebig groß werden:

my h = 0
my k = 1
while (k <= 100) { h = h + 1 / k; k = k + 1 }
println(h)
# 14466636279520351160221518043104131447711/2788815009188499086581352357412492142272

Das ist die exakte Summe 1 + 1/2 + … + 1/100. Mit numerator und denominator kommst du an die beiden Teile:

val r = 3/4
println(r.numerator)     # 3
println(r.denominator)   # 4

Brüche werden immer gekürzt gespeichert, das Vorzeichen steht im Zähler. Ein Int ist der Bruch n/1: (5).numerator ist 5, (5).denominator ist 1.

Wann float(...) sinnvoll ist#

Exaktheit hat einen Preis: Bei langen Rechenketten mit Brüchen wachsen Zähler und Nenner, und jede Rechnung wird langsamer. Typisch ist eine Schleife, die einen Wert immer wieder mit einem Dezimalfaktor multipliziert:

my v = 4
for 1..40 { v = v * -0.95 + 0.2 }
println(v.denominator)   # eine Zahl mit 52 Stellen

Wenn das Ergebnis nicht exakt sein muss, etwa bei Animationen, Physik oder Statistik, mach den Faktor zur Gleitkommazahl. Danach bleibt die Rechnung schnell, weil Float ansteckt:

my v = 4
for 1..40 { v = v * float(-0.95) + 0.2 }
println(float?(v))       # true

Für Geldbeträge, Brüche, Zählerstände und alles, bei dem jede Stelle zählt, bleibst du bei Int und Rat.

Vergleichen#

==, <, >, <=, >= und <=> vergleichen Zahlen nach ihrem Wert, über alle drei Arten hinweg:

println(2 == 4/2)          # true
println(1/2 == 0.5)        # true
println(0.1 + 0.2 == 0.3)  # true (alles Brüche, also exakt)

Mit Gleitkommazahlen gelten die üblichen Rundungsfehler:

println(float(0.1) + float(0.2) == float(0.3))   # false

Auch zwischen einer exakten Zahl und einer Gleitkommazahl zählt der genaue Wert, nicht der gerundete: (2 ** 64 + 1) > float(2 ** 64) ist true, und 1/3 == float(1/3) ist false, weil float(1/3) nur ungefähr ein Drittel ist.

NaN (etwa (-1) ** 0.5) ist keiner Zahl gleich, auch sich selbst nicht, und hat keine Ordnung: ==, <, <=, >, >= sind false, != ist true, <=> ist ein Fehler. Inf ist größer als jede exakte Zahl, auch eine jenseits des Float-Bereichs.

=== ist strenger und vergleicht auch die Art: 2 === 4/2 ist false, 2 === float(2) ebenso, 1/2 === 0.5 ist true (beides Brüche).

Eine Zahl ist nie gleich einem String: 1 == "1" ist false (kein Fehler), 1 < "1" dagegen ein Typfehler. Wandle vorher mit num(...) oder str(...) um.

Ranges mit Brüchen#

Eine Range rechnet mit derselben Art wie ihre Grenzen und ihr Schritt. Mit einem Bruch sind alle Werte exakte Brüche, sie werden nicht aufsummiert, und das Ende geht nicht verloren:

println(collect(0..0.3 by 0.1))   # [0, 0.1, 0.2, 0.3]
println(collect(1..2 by 1/3))     # [1, 4/3, 5/3, 2]
println(collect(0.5..<3))         # [0.5, 1.5, 2.5]
println(0.3 in 0..1 by 0.1)       # true

Ints jenseits von 64 Bit gehen genauso (9007199254740992..9007199254740995).

Anzeige#

WertAnzeige
Intalle Ziffern: 1267650600228229401496703205376
Rat mit Nenner 1wie ein Int: 4 / 2 zeigt 2
Rat, dessen Nenner nur die Faktoren 2 und 5 hatals Dezimalzahl: 1/4 zeigt 0.25
jeder andere Ratals Bruch: 1/3 zeigt 1/3
Floathöchstens 15 gültige Stellen: 1.4142135623731
ganzzahlige Floatohne Nachkommastellen: Math.sqrt(4) zeigt 2

Die Anzeige verrät die Art also nicht immer. 2 kann ein Int, ein Rat oder eine Float sein. Wenn es darauf ankommt, frag mit int?, rat? oder float?.

Die Dezimalanzeige eines Bruchs ist auf 15 gültige Stellen gerundet, der Wert selbst bleibt exakt. 1 / (2 ** 70) zeigt 8.470329472543e-22 und rechnet trotzdem mit dem genauen Bruch weiter.

Umwandeln und Runden#

FunktionErgebnis
num("12"), num("2.5")Zahl aus einem String, gelesen wie ein Literal: 12 ist ein Int, 2.5 ein Bruch
int(x)Int, Richtung null abgeschnitten: int(-7/2) ist -3
float(x)Float
rat(x)Rat: rat("3/4"), rat(5); aus einer Gleitkommazahl ihr genauer Wert (rat(float(0.1)) == float(0.1)), die angezeigte Dezimalzahl mit rat(str(x))
str(x)die Anzeige als String
Math.floor(x)Int, abgerundet: Math.floor(-7/2) ist -4
Math.ceil(x)Int, aufgerundet
Math.round(x)Int, Hälften nach oben: Math.round(5/2) ist 3, Math.round(-5/2) ist -2
Math.trunc(x)Int, Richtung null
Math.abs, Math.min, Math.maxbehalten die Art: Math.min(1/3, 1/2) ist 1/3

Die Rundungsfunktionen rechnen bei Brüchen exakt und liefern immer einen Int, auch für eine Gleitkommazahl; NaN und Inf haben keinen und sind ein Fehler (wie bei int(...)). Brauchst du aus einer Rechnung mit Math.sqrt eine ganze Zahl, etwa als Index oder Anzahl, nimm sie:

import Antra::Math

val n = Math.floor(Math.sqrt(10))
println(n)          # 3
println(int?(n))    # true

Zahlen als Map-Keys#

Map-Keys sind Text. Eine Zahl wird als Key zu ihrem exakten Text: Ein Bruch mit endlicher Dezimaldarstellung wird voll ausgeschrieben (0.5), jeder andere als n/d, und eine Gleitkommazahl mit so vielen Stellen, wie sie braucht. Gleiche Werte landen deshalb im selben Eintrag, verschiedene nie:

my m = %{}
m[1/2] := "a"
m[0.5] := "b"
println(m)          # %{ '0.5' => 'b' }
println(m["0.5"])   # b

Für Set-Elemente gilt dasselbe: %<1, 2/2> hat ein Element, zwei Gleitkommazahlen, die sich nur in der letzten Stelle unterscheiden, sind zwei.

Grenzen#

  • Bit-Operatoren (&, |, ^, <<, >>, ~) arbeiten auf 64-Bit- Ganzzahlen. Mit einem größeren Int sind sie ein Fehler, ebenso Int.bit_count, Int.bit_length, die Rotationen und Int.to_hex. Ein ganzer Bruch oder eine ganze Gleitkommazahl zählt als ihr Int ((6/2) & 1), 2.5 & 1 ist ein Fehler statt abgeschnitten. Ab 64 Stellen schiebt << alles hinaus (1 << 64 ist 0), >> lässt 0 oder -1 übrig; ein negativer Zähler ist ein Fehler.
  • Index und Anzahl müssen ganz sein: xs[4/2] und take(xs, 2.0) gehen, xs[1.5] und take(xs, 1.5) sind ein Fehler.
  • Riesige Exponenten: ** mit einem Int-Exponenten jenseits von 32 Bit ist ein Fehler, außer für die Basis 0, 1 und -1.
  • JSON hat nur Dezimalzahlen. JSON.encode schreibt einen Bruch mit endlicher Dezimaldarstellung exakt (1.5, 0.25), Gleitkommazahlen mit allen nötigen Stellen und große Ganzzahlen vollständig. Für 1/3 gibt es keine exakte JSON-Zahl, das ist ein Fehler; schreib float(1/3), wenn eine Näherung reicht. JSON.parse liest Dezimalzahlen als Gleitkommazahlen.
  • Gleitkommazahlen haben die üblichen Grenzen: etwa 15 gültige Stellen, Rundungsfehler, und Math.sqrt(-1) ist ein Fehler. Eine exakte Zahl jenseits des Float-Bereichs (float(10 ** 400), auch in einer Rechnung mit einer Float) ist ein Fehler statt still Inf.
  • Literale dürfen höchstens 10 000 Stellen ergeben (siehe Literale).

Kurzreferenz#

  • Int und Rat sind exakt und unbegrenzt, Float ist ungenau.
  • 0.5 ist ein Bruch, keine Gleitkommazahl.
  • Division liefert einen Bruch, auch 4 / 2.
  • Float entsteht nur durch Math.*-Funktionen, ** mit Bruch-Exponent, JSON.parse und float(...), und steckt an.
  • ** mit Int-Exponent bleibt exakt: (1/2) ** 2 ist 1/4.
  • Die Art folgt aus der Herkunft, nicht aus dem Wert.
  • Math.floor & Co. liefern einen Int.
  • Lange Bruch-Ketten, die nicht exakt sein müssen: den Faktor mit float(...) umwandeln (v * float(0.95)).

Quelle dieser Seite