Skip to Content
i documentation
A tour of the language

Values and data

Define names and build immutable numbers, strings, records, tuples, lists, and labelled values.

Values are the things a program works with: numbers, text, functions, and data built from other values. This page shows how to bind a value to a name, combine values, and inspect the result. You can use these forms to describe the data your functions receive and return.

src/main.i
(
	i/core
	i/stdio
)

main = () {
	answer = 42
	stdio.print(core.inspect(answer))
}
42

Names and definitions

answer = 42 is a binding: it gives the name answer the value 42. A local name is available from the next statement to the end of its block. Names use lowercase letters, digits, and underscores, starting with a lowercase letter or one underscore; answer, line_count, and _unused are examples.

There are no keywords. main is a name, and = is the symbol for a binding. A comment starts with // and ends at the line end. The braces around the body of main form a block; its final expression gives its value.

core.inspect takes any value and returns its text form as a String. Here, stdio.print prints that string. Inspection is useful when learning a new data form: it shows strings with quotes and lists with brackets.

Integers

src/main.i
(
	i/core
	i/stdio
)

main = () {
	whole = 1_000 + 0x10
	stdio.print(core.inspect(whole))
	stdio.print(core.inspect(7 / 2))
	stdio.print(core.inspect(7 % 2))
	stdio.print(core.inspect(-7 / 2))
}
1016
3
1
-3

An Int is a whole number of any size; it does not overflow. Write decimal digits, or use 0x, 0o, and 0b for hexadecimal, octal, and binary. Underscores separate digit groups. Integer arithmetic uses +, -, *, /, and %; division rounds toward zero, and % gives the remainder.

Floats

src/main.i
(
	i/core
	i/stdio
)

main = () {
	stdio.print(core.inspect(7.0 /. 2.0))
	stdio.print(core.inspect(1.5 +. 2.0))
	stdio.print(core.inspect(-.1.5))
	stdio.print(
		core.inspect(core.to_float(7)),
	)
}
3.5
3.5
-1.5
7.0

A Float is a finite binary64 number. Its arithmetic uses separate operators: +., -., *., and /.. Negation is - for an integer and -. for a float. There is no implicit conversion between the two number types; core.to_float converts an integer explicitly.

Strings and interpolation

src/main.i
(
	i/core
	i/stdio
)

main = () {
	name = "Ada"
	stdio.print("Hello, \(name)!")
	stdio.print("count: \(core.inspect(3))")
	stdio.print("a" <> "b")
}
Hello, Ada!
count: 3
ab

A String holds Unicode text. Double quotes enclose an ordinary string, and <> joins strings. Interpolation writes an expression inside \(…). That expression must already return a string: name does, while the integer 3 needs core.inspect first.

Multi-line strings

src/main.i
(
	i/core
	i/stdio
)

main = () {
	lines = """
		first
		second
		"""
	stdio.print(core.inspect(lines))
}
"first\nsecond"

A multi-line string opens with three quotes followed immediately by a line break. The closing quotes stand on their own line. Their indentation is removed from each content line; the opening and closing line breaks are left out of the value. The inspected output makes the remaining line break visible as \n. The formatter rewrites this string as "first\nsecond", using an escape for the line break.

See Appendix C for the complete string grammar, including raw strings and other escapes.

Records, tuples, and updates

src/main.i
(
	i/core
	i/stdio
)

main = () {
	original = (name = "Ada", score = 4)
	updated = (..original, score = original.score + 1)
	pair = (original.name, updated.score)
	stdio.print(core.inspect(original))
	stdio.print(core.inspect(updated))
	stdio.print(core.inspect((pair.0, pair.1)))
	stdio.print(core.inspect(("one")))
}
(name = "Ada", score = 4)
(name = "Ada", score = 5)
("Ada", 5)
("one")

A record is data with labelled fields, such as name and score. Read a field with .name. A tuple is data with numbered positions; .0 reads its first position. Both are forms of data written with parentheses, and positions count from zero. The number of positions and the set of labels together are its shape. Labels name fields, rather than creating local bindings.

Every value is immutable, including every record, list, and string. “Changing” a record means making another record. The spread ..original copies its fields into new data; the later score = … replaces that field in the new value. The original record still has score 4, as the first output line shows.

An empty data value is (). One positional item still makes data: ("one") is a one-position tuple, whose inspected form retains the parentheses. Use braces to group an expression rather than putting it in a tuple.

Lists

src/main.i
(
	i/core
	i/stdio
)

main = () {
	tail = [2, 3]
	numbers = [1, ..tail]
	stdio.print(core.inspect(numbers))
	stdio.print(core.inspect(tail))
	stdio.print(core.inspect([]))
}
[1, 2, 3]
[2, 3]
[]

A list is an ordered sequence written [a, b, c]. [] is empty, and [x, ..tail] puts x before the items of tail, leaving tail unchanged.

Booleans

src/main.i
(
	i/core
	i/stdio
)

main = () {
	left = 5 + 2
	right = 1 + 1
	stdio.print(
		core.inspect((True && False, True || False, core.not(True))),
	)
	stdio.print(core.inspect((left > right, 1.5 <. 2.0)))
	stdio.print(core.inspect((left == right, left != right)))
}
(False, True, False)
(True, True)
(False, True)

The boolean values are True and False, with type Bool. && and || combine them; core.not reverses a boolean. Integer comparisons use <, <=, >, and >=; float comparisons add a dot. Equality uses ==, and inequality uses !=.

Labelled data

src/main.i
(
	i/core
	i/stdio
)

Present = (Int)
Absent = ()
Maybe = Present | Absent

main = () {
	found : Maybe = Present(7)
	stdio.print(core.inspect(found))
	stdio.print(core.inspect(Absent))
}
Present(7)
Absent

A labelled type gives data its own identity. Present = (Int) defines a labelled type and its constructor Present, which wraps an integer. Absent = () defines a labelled value with no fields: write Absent, without calling it.

A union contains values from any of its member types. Maybe names the closed union Present | Absent; its values come from those two alternatives. The annotation found : Maybe says that found belongs to this union. The types page explains annotations and the difference between naming a union and defining a labelled type.

Bit arrays

src/main.i
(
	i/core
	i/stdio
)

main = () {
	packet = <<65, 2:u3>>
	stdio.print(core.inspect(packet))
	stdio.print(
		core.inspect(core.bit_size(packet)),
	)
}
<<65, 2:u3>>
11

A bit array is a sequence of bits, written between << and >>. An unmarked integer segment takes eight bits; :u3 takes three unsigned bits, so this value has eleven bits. Bit arrays support explicit widths, byte order, and text encodings; see standard §8 for those formats.

See also