Records and functions
Grouping data in records and calling functions with explicit arguments.
A record groups fields with names and types. When you pass it to a function, the function receives an independent copy, as it does with a number.
Declaring one
# A position on the integer grid.
record Point
x: i64
y: i64
Each field declares a name and a type on an indented line. A field can contain another record: Rect contains two point values:
# A rectangle, by two corners.
record Rect
corner: Point
extent: Point
Building one: every field is named
There is one way to make a record, and it names each field:
function main()
a = Point(x: 0, y: 0)
b = Point(x: 3, y: 4)
print("distance squared: ", a.dist2(b: b))
print("midpoint x: ", a.midpoint(b: b).x)
Point(3, 4) is invalid because the field names are missing. The compiler reports the error and suggests a fix:
$ heroes check points.hero
error[missing_label]: `Point`'s fields are always named — this one is `x`
at points.hero:46:15
|
46 | a = Point(0, 0)
| ^
fix (certain): write `x: `
With field names, Point(y: 4, x: 3) and Point(x: 3, y: 4) describe the same point. Argument order does not change which value is assigned to each field.
Functions, and where the names become mandatory
function dist2(a: Point, b: Point) -> i64
dx = a.x - b.x
dy = a.y - b.y
return dx * dx + dy * dy
A function declaration specifies the type of every parameter and, after ->, the result type. Inside the body, dx = … gives a value a name and the compiler infers its type. You can therefore find the input and output types by reading the declaration.
When two parameters of a function share a type, the call must name them. This applies to dist2, which takes two Point values:
$ heroes check points.hero
error[needs_label]: two of `dist2`'s parameters are `Point`, so every one of them is named at the call site — this one is `b`
at points.hero:48:41
|
48 | print("distance squared: ", a.dist2(b))
| ^
fix (certain): write `b: `
A type alone cannot distinguish two arguments of that type. Names make each argument’s role explicit. If the types differ, the compiler can distinguish them and labels are optional.
Calls with dot syntax
x.f(y) means f(x, y). Dot syntax is another way to write an ordinary function call. It does not define a method, a function attached to a type; the compiler determines which function to call before execution.
# Reads as a sentence, and it is one function call: `translate(r, by)`.
function translate(r: Rect, by: Point) -> Rect
moved = Point(x: r.corner.x + by.x, y: r.corner.y + by.y)
return Rect(corner: moved, extent: r.extent)
unit.translate(p).area() and area(translate(unit, p)) perform the same calls. Choose the form that reads best; dot syntax lets you read the operations from left to right.
Tests live next to what they test
test "the 3-4-5 triangle"
assert dist2(a: Point(x: 0, y: 0), b: Point(x: 3, y: 4)) == 25
test "a rectangle keeps its size when it moves"
unit = Rect(corner: Point(x: 0, y: 0), extent: Point(x: 2, y: 3))
assert unit.translate(Point(x: 10, y: 10)).area() == unit.area()
A test block is declared outside functions, beside the code it checks. Ordinary builds ignore it. Here the second test checks that moving a rectangle does not change its area.
$ heroes test examples/gallery/01-points.hero
ok "the 3-4-5 triangle"
ok "a rectangle keeps its size when it moves"
2 tests, all passed
Function limits
Heroes has no inheritance, interfaces, overloading, meaning multiple functions with the same name, or default parameter values. You cannot define functions with a variable number of arguments. These limits reduce the rules needed to understand a call.
Next: Variants and match, where a value can be one of several shapes and forgetting one stops the build. Back to the chapter list. ← Previous: Errors
Run a program of your own: it takes four commands. Or take the whole language in one file, and every program already written in it.