Chapter 2
Types
| Type | Values | Notes |
|---|---|---|
Int |
42, -7 |
64-bit signed |
Float |
3.14, 2.0 |
64-bit; a decimal point is required |
Bool |
true, false |
|
Str |
"hi", "a\nb" |
escapes: \n \t \\ \" |
Unit |
() |
what println and main return; () is its one value, for a branch with nothing to say |
Chan(a) |
channel() |
see Concurrency |
Map(k, v) |
map_new() |
a hash table; see Maps |
StrBuf |
strbuf() |
bytes on their way to a string |
(A, B) -> C |
\x, y -> ... |
function values; () -> C and \ -> ... take nothing |
| user types | Circle(1.0) |
declared with type |
Types are inferred; annotations are optional and only ever needed to resolve an ambiguity or to document intent:
fn area(r: Float) -> Float = 3.14159 * r * r
There are no implicit conversions. 1 + 2.0 is a type error; to_float and to_int cross between them explicitly.
A type alias is a name for a type: type Meters = Float, or with parameters, type Pair(a) = (a, a). It is a name and nothing more β every mention is read as what it names before the checker sees the program, so a Meters and a Float are one type, an alias prints as what it names, and an alias that names itself is refused. A type to be kept apart from what it holds is a constructor instead: type Meters with Meters(v: Float) is a new type, and m.v is the number.
Int is 64-bit two's complement and its arithmetic is the machine's: +, - and * wrap round at the ends, as they do in C, Go and a release build of Rust. The two cases the machine has no one answer for are decided: / and % by zero end the program, saying where β a number that is silently wrong is worse than a stop β and the smallest integer divided by -1 wraps to itself, with a remainder of zero. to_int truncates toward zero, and past the ends of Int gives the nearest end; a NaN gives zero. float_bits(x) is the 64 bits of a float as the Int they are and bits_float(n) the float those bits spell β no conversion, for a file format or a sort key that carries a float as bytes. A shift by 64 or more, or by less than nothing, is zero. There are no narrower numbers as values: a byte read from a string or a buffer is an Int, and the narrow names β U8, I32, F32 and the rest β say how storage and C signatures are laid out; see Buffers and Calling C.
A float literal may carry an exponent β 1.0e9, 2.5e-3, 1e6, with or without a dot before the e β since a digit run can be followed by nothing else that means anything. The canonical spelling has no exponent, so rill fmt writes 1.0e9 back as 1000000000.0.
Float arithmetic may be contracted: a multiply and the add that consumes it can become one fused instruction, rounding once instead of twice. The answer is nearer the true one, not further, but it is not the answer two separate roundings give. C, C++ and Go do this by default and Rill follows them; Rust and Zig do not. Nothing else about float arithmetic is loosened β no reassociation, no assumptions about NaN or infinity, no reciprocals. On a Mandelbrot it is worth about six percent.
Algebraic data types
type Shape Circle(r: Float) Rect(w: Float, h: Float) type List(a) # generic, and recursive Nil Cons(head: a, tail: List(a))
Field names are optional (Rect(Float, Float) is equivalent), but a named field can be read by its name β see Fields. Constructor names are global and must start with an uppercase letter; a type and one of its constructors may share a name.
Fields
A value whose type has one constructor is read with a dot:
type Point P(x: Float, y: Float) fn dist(a: Point, b: Point) = dx = a.x - b.x dy = a.y - b.y sqrt(dx * dx + dy * dy)
One constructor is the whole condition, and it is the condition a binding already had: P(x, y) = p reads as a binding because it cannot fail, and p.x for the same reason. A type with several constructors reads the same way for a field every constructor has, of one type β s.name on a Circle(r: Float, name: Str) / Rect(w: Float, h: Float, name: Str) is certainly there whichever it is, and becomes the match a program would have written. A field some constructor lacks is not certainly there, so match is how those are read; the error names the constructor that lacks it. Updating works the same way: { s | name = "disc" } rebuilds whichever constructor s is.
The dot works on anything, not only a name: mk(1).y, p.pos.x. What it does not do is change anything. A record is a value; the way to a different one is to build it:
{ p | health = p.health - 35 }
Everything p had, apart from these β read the | as "except". It is a call to the constructor, the named fields taking the new values and every other one read off p, which is evaluated once however many it fills in. Written out, Player(p.name, p.pos, p.health - 35, p.kills) builds the same value and says which fields it keeps as well; either way p is what it was. Writing p.x = 3 is refused, and says this.
A field read does not need the type in front of it known where the dot is written. It is answered once the whole function is inferred, so a parameter read by .x on one line and handed to something that says its type on the next needs no annotation, whichever order the lines are in. When nothing in the function says the type, the fields do: a value read by .x and .y is of the one single-constructor type that has an x and a y. Two types that both have them is the one case an annotation is still for, and the error names them; for a field no type has, it names the types that have the others. An update is answered the same way, by the fields it sets.
A dot means one of two things and the compiler tells them apart by what the name in front of it is: a module alias (geo.area, from import "geo" as geo) or a value with fields. An alias wins where a program has both.