Types¶
Candela is statically typed with inference. Most types go unwritten, but every expression has one, and the compiler rejects a program whose types do not line up before it runs.
The built-in types¶
| Type | Written as | Example values |
|---|---|---|
| Integer | int |
0, 42, -7 |
| Floating point | float |
1.5, 0.0, -2.75 |
| String | string |
"hello", "" |
| Boolean | bool |
true, false |
| Absent value | null |
null |
| List | T[] |
[1, 2, 3] |
| Map | {K: V} |
{"a": 1} |
| Function | fn(A) -> R |
fn(x) { return x; } |
int is a signed 64-bit integer and float is double precision. A numeric
literal with a decimal point is a float; without one it is an int. An
exponent makes a float too, with a point or without: 1e3, 2.5e-1,
6.02e23. An int literal outside -9223372036854775808 to 9223372036854775807
is a compile error at the literal; give it a decimal point or an exponent to
write it as a float instead. A float literal past what double precision
holds, roughly 1.8e308, is a compile error at the literal too, rather than an
infinity the program then runs on.
Arithmetic that leaves the range wraps around it rather than raising, so a sum past the top comes back at the bottom.
Lists and maps are covered in Collections, functions in
Functions, and your own types in Enums and in
Structs below. A function's type is inferred wherever the value
says it; write fn(A) -> R where a declaration has to say what it accepts,
such as a struct field or a parameter.
Inspecting a type¶
type(value) returns the type as a string, which is the quickest way to check
what the compiler inferred.
fn main() {
print(type("hi"), type(1), type(1.5), type(true), type(null));
print(type([1, 2]), type({"a": 1}));
}
Every answer is written the way the same type is written in a program: a struct
or an enum by its declared name, a dynamic value as any, and a function as
fn(A) -> R. It is the type the compiler inferred, so a value it could not pin
reads any however the program later fills it.
The predicates is_int, is_float, is_str, is_bool, is_list, is_map,
and is_null answer the same question as a bool.
No implicit conversion¶
Candela never converts a value behind your back. Mixing an int and a float
in the same arithmetic, or adding a number to a string, is a compile error.
Convert first with int, float, str, or bool.
fn main() {
let count = 3;
let rate = 1.5;
print(float(count) * rate);
print("count: " + str(count));
}
int accepts a string or a float, float accepts a string or an int,
str accepts any value, and bool accepts the strings "true" and "false".
A conversion that cannot succeed raises at runtime; see
Error handling.
Truthiness¶
There is none, and none is added for you. 0, "" and null are not tests,
and a condition of any type but bool is a compile error at the condition. Write
the comparison out.
null¶
null is the value of an expression that has nothing to return, such as a
function that returns without a value. It is its own type: no other type accepts
it, so a variable is never implicitly empty. Test for it with is_null.
Passing null to print produces no output, so print a placeholder rather than
the value itself when a value may be absent. For a value that is optional by
design, prefer the Option enum from the standard library over null.
Where you write a type¶
Type annotations appear on struct fields, enum variant payloads, the signature blocks that declare foreign functions, and, where you want them, function parameters and return types. The type grammar is the same in all of them.
int,float,bool,string,null: the built-in types.T[]: a list ofT, for examplestring[]orint[][].{K: V}: a map fromKtoV, for example{string: int}.- A struct or enum name: that type.
A|B: a union, a value that is either anAor aB.fn(A, B) -> R: a function taking anAand aBand returning anR. Leave the arrow off for one that returns nothing.(T): parentheses, which group a type the way they group an expression.any: a slot whose type is decided by the value, written on a struct field, a parameter, or an enum payload.
[] after a function type belongs to its return type, so fn(int) -> int[] is
a function returning a list of ints. Parenthesise to put the function itself in
a list: (fn(int) -> int)[]. The same rule reads a map value type,
{string: fn(int) -> int}, as a map of functions.
An annotation is not only checked against the value. Where a collection carries
no element type of its own the annotation supplies one: an empty list has no
element type and an empty map has neither a key nor a value type, so a parameter
declared Value[] decides that the body sees Value elements, a parameter
declared {string: Value} decides that it sees string keys and Value
values, and a -> Value[] or -> {string: Value} return type decides what the
caller gets back from return [] or return {}. An annotation never overrides
a type the value does have, so passing [1, 2] to a Value[] parameter is
still an error. A let takes no annotation, so an empty collection in a local
takes its element types from the first value put into it instead.
Structs¶
A struct groups named fields into one type. Declare it at the top level, with a type for every field.
Build a value with Name { field: value, ... }, giving every field. Read and
write a field with a dot.
struct Point {
x: int,
y: int,
}
fn main() {
let p = Point { x: 0, y: 0 };
p.x = 10;
print(p.x + p.y);
}
A field can hold any type, including a list, a map, or a union.
struct Item {
name: string,
tags: string[],
counts: {string: int},
id: int|string,
}
fn main() {
let it = Item { name: "widget", tags: ["new"], counts: {"sold": 2}, id: "w-1" };
print(it.name, it.tags, it.counts, it.id);
}
A field declared any takes a value of any type, and reading it gives an any
back; name the type again with as_int, as_str or another downcast.
To give a struct behaviour, write an impl block; see Methods.
To let one struct hold a field of whatever type you name at the point of use,
give it a type parameter; see Generics.