Values
Aqua is all about combining data and computations. The runtime for the compiled Aqua code, AquaVM, tracks what data comes from what origin, which constitutes the foundation for distributed systems security. That approach, driven by π-calculus and security considerations of open-by-default networks and distributed applications as custom application protocols, also puts constraints on the language that configures it.
Values in Aqua are backed by VDS (Verifiable Data Structures) in the runtime. All operations on values must keep the authenticity of data, proved by signatures under the hood.
That's why values are immutable. Changing the value effectively makes a new one:
aqua
x = "hello"y = "world"-- despite the sources of x and y, z's origin is "peer 1"-- and we can trust value of z as much as we trust "peer 1"on "peer 1":z <- concat(x, y)
aqua
x = "hello"y = "world"-- despite the sources of x and y, z's origin is "peer 1"-- and we can trust value of z as much as we trust "peer 1"on "peer 1":z <- concat(x, y)
More on that in the Security section. Now let's see how we can work with values inside the language.
Arguments
Function arguments are available within the whole function body.
aqua
func foo(arg: i32, log: string -> ()):-- Use data argumentsbar(arg)-- Arguments can have arrow type and be used as stringslog("Wrote arg to responses")
aqua
func foo(arg: i32, log: string -> ()):-- Use data argumentsbar(arg)-- Arguments can have arrow type and be used as stringslog("Wrote arg to responses")
Return values
You can assign the results of an arrow call to a name and use this returned value in the code below.
aqua
-- Imagine a Stringify service that's always availableservice Stringify("stringify"):i32ToStr(arg: i32) -> string-- Define the result type of a functionfunc bar(arg: i32) -> string:-- Make a value, name it xx <- Stringify.i32ToStr(arg)-- Starting from there, you can use x-- Pass x out of the function scope as the return value<- xfunc foo(arg: i32, log: *string):-- Use bar to convert arg to string, push that string-- to logs stream, return nothinglog <- bar(arg)
aqua
-- Imagine a Stringify service that's always availableservice Stringify("stringify"):i32ToStr(arg: i32) -> string-- Define the result type of a functionfunc bar(arg: i32) -> string:-- Make a value, name it xx <- Stringify.i32ToStr(arg)-- Starting from there, you can use x-- Pass x out of the function scope as the return value<- xfunc foo(arg: i32, log: *string):-- Use bar to convert arg to string, push that string-- to logs stream, return nothinglog <- bar(arg)
Aqua functions may return more than one value.
aqua
-- Define return types as a comma separated listfunc myFunc() -> bool, string:-- Function must return values for all defined types<- true, "successful execution"func otherFunc():-- Call a function, don't use returnsmyFunc()-- Get any number of results out of the functionflag <- myFunc()-- Push results to a streamresults: *stringis_ok, results <- myFunc()if is_ok:-- We know that it contains successful resultfoo(results!)
aqua
-- Define return types as a comma separated listfunc myFunc() -> bool, string:-- Function must return values for all defined types<- true, "successful execution"func otherFunc():-- Call a function, don't use returnsmyFunc()-- Get any number of results out of the functionflag <- myFunc()-- Push results to a streamresults: *stringis_ok, results <- myFunc()if is_ok:-- We know that it contains successful resultfoo(results!)
Literals
Aqua supports just a few literals: numbers, double-quoted strings, booleans and nil
.
aqua
-- String literals cannot contain double quotes-- No single-quoted strings allowed, no escape chars.foo("double quoted string literal")-- Booleans are true or falseif x == false:foo("false is a literal")-- Numbers are different-- Any number:bar(1)-- Signed number:bar(-1)-- Float:bar(-0.2)func takesMaybe(arg: ?string): ...-- nil can be passed in every place-- where a read-only collection fitstakesMaybe(nil)
aqua
-- String literals cannot contain double quotes-- No single-quoted strings allowed, no escape chars.foo("double quoted string literal")-- Booleans are true or falseif x == false:foo("false is a literal")-- Numbers are different-- Any number:bar(1)-- Signed number:bar(-1)-- Float:bar(-0.2)func takesMaybe(arg: ?string): ...-- nil can be passed in every place-- where a read-only collection fitstakesMaybe(nil)
Operators
Below is the table of operators in Aqua, sorted by precedence.
Each operator has a link to the section that describes it in more detail.
Operators | Notation | Precedence | Associativity | Reference |
---|---|---|---|---|
Unary logical negation | ! | 1 | Logical operators | |
Exponentiation | ** | 2 | Left | Arithmetic operators |
Multiplication and division | * , / , % | 3 | Left | Arithmetic operators |
Addition and subtraction | + , - | 4 | Left | Arithmetic operators |
Comparison | < , > , <= , >= | 5 | Comparison operators | |
Equality | == , != | 6 | Equality operators | |
Logical conjunction | && | 7 | Left | Logical operators |
Logical disjunction | || | 8 | Left | Logical operators |
Arithmetic operators
Aqua supports +
, -
, *
, /
, **
(power), %
(reminder) for integer values.
aqua
func arithmetics(a: i32, b: i32) -> i32:c = a + bd = c - ae = d * cf = e / d-- power 3: unsigned number expectedg = f ** 3-- remaindere = g % f-- It is possible to use arithmetics-- anywhere an integer is required<- a + (b - c) + d * (e - 6)
aqua
func arithmetics(a: i32, b: i32) -> i32:c = a + bd = c - ae = d * cf = e / d-- power 3: unsigned number expectedg = f ** 3-- remaindere = g % f-- It is possible to use arithmetics-- anywhere an integer is required<- a + (b - c) + d * (e - 6)
Precedence of operators from highest to lowest:
**
*
,/
,%
(left associative)+
,-
(left associative)
Comparison operators
Aqua supports <
, >
, <=
, >=
for integer values.
Result of a comparison operator has type bool
.
aqua
func comparison(a: i32, b: i32) -> bool:c = a < bd = a > be = a <= bf = a >= b<- f
aqua
func comparison(a: i32, b: i32) -> bool:c = a < bd = a > be = a <= bf = a >= b<- f
Comparison operators have lower precedence than arithmetic operators.
aqua
-- This is equivalent to (a + b) < (c * d)v = a + b < c * d
aqua
-- This is equivalent to (a + b) < (c * d)v = a + b < c * d
Equality operators
Aqua supports ==
, !=
for scalars, collections, and structures.
Result of an equality operator has type bool
.
aqua
func equality(a: i32, b: i32) -> bool:c = a == bd = a != b<- d
aqua
func equality(a: i32, b: i32) -> bool:c = a == bd = a != b<- d
Equality operators have lower precedence than comparison operators.
aqua
-- This is equivalent to ((a + b) < c) == (d >= (e * f))v = a + b < c == d >= e * f
aqua
-- This is equivalent to ((a + b) < c) == (d >= (e * f))v = a + b < c == d >= e * f
Logical operators
Aqua supports !
, ||
, &&
for boolean values.
Result of a logical operator has type bool
.
aqua
func logic(a: bool, b: bool) -> bool:c = !ad = a || be = a && b<- (e || c) && d || !!true
aqua
func logic(a: bool, b: bool) -> bool:c = !ad = a || be = a && b<- (e || c) && d || !!true
Precedence of operators from highest to lowest:
!
&&
||
Logical operators have lower precedence than equality operators, except for !
which has higher precedence than all other operators.
aqua
-- This is equivalent to (((a + b) >= c) && ((d * e) < f)) || (g != h)v = a + b >= c && d * e < f || g != h
aqua
-- This is equivalent to (((a + b) >= c) && ((d * e) < f)) || (g != h)v = a + b >= c && d * e < f || g != h
Operators ||
and &&
are short-circuiting, so the right operand is evaluated
only if the left operand does not determine the result.
aqua
service Launcher("launcher"):launch(what: string) -> boolfunc foo() -> bool:check = 1 < 2-- Launcher.launch won't be called!res = check || Launcher.launch("rockets")<- res
aqua
service Launcher("launcher"):launch(what: string) -> boolfunc foo() -> bool:check = 1 < 2-- Launcher.launch won't be called!res = check || Launcher.launch("rockets")<- res
Collections
With Aqua it is possible to create a stream, fill it with values, and use in place of any collection:
aqua
-- foo returns an arrayfunc foo() -> []string:-- Initiate a typed streamret: *string-- Push values into the streamret <<- "first"ret <<- "second"-- Return the stream in place of the array<- ret
aqua
-- foo returns an arrayfunc foo() -> []string:-- Initiate a typed streamret: *string-- Push values into the streamret <<- "first"ret <<- "second"-- Return the stream in place of the array<- ret
Aqua provides syntax sugar for creating any of the collection types with [ ... ]
for arrays, ?[ ... ]
for optional values, *[ ... ]
for streams.
aqua
func foo() -> []string, ?bool, *u32:<- ["string1", "string2"], ?[true, false], *[1, 3, 5]
aqua
func foo() -> []string, ?bool, *u32:<- ["string1", "string2"], ?[true, false], *[1, 3, 5]
The ?[]
expression takes any number of arguments, but returns an optional value that contains no more than one value. This is done by trying to yield these values one by one. The first value that yields without an error will be added to the resulting option.
aqua
func getFlag(maybeFlagA: ?bool, maybeFlagB: ?bool, default: bool) -> bool:res = ?[maybeFlagA!, maybeFlagB!, default]<- res!
aqua
func getFlag(maybeFlagA: ?bool, maybeFlagB: ?bool, default: bool) -> bool:res = ?[maybeFlagA!, maybeFlagB!, default]<- res!
The length of a collection can be obtained using the .length
command.
aqua
func getLength(arr: []string) -> u32:<- arr.length
aqua
func getLength(arr: []string) -> u32:<- arr.length
It is not possible yet to get an element by index or get a length directly from the collection creation expression, e.g. ?[maybe!].length
, [1, 2, 3][index]
are not allowed.
It is possible to fill any immutable collection with an empty one using nil
.
aqua
func empty() -> []string, ?bool:<- nil, nil
aqua
func empty() -> []string, ?bool:<- nil, nil
Getters
In Aqua, you can use a getter to peek into a field of a product or indexed element in an array.
aqua
data Sub:sub: stringdata Example:field: u32arr: []Subchild: Subfunc foo(e: Example):bar(e.field) -- u32bar(e.child) -- Subbar(e.child.sub) -- stringbar(e.arr) -- []Subbar(e.arr!) -- gets the 0 elementbar(e.arr!.sub) -- stringbar(e.arr!2) -- gets the 2nd elementbar(e.arr!2.sub) -- stringbar(e.arr[2]) -- gets the 2nd elementbar(e.arr[2].sub) -- stringbar(e.arr[e.field]) -- can use any scalar as index with [] syntax
aqua
data Sub:sub: stringdata Example:field: u32arr: []Subchild: Subfunc foo(e: Example):bar(e.field) -- u32bar(e.child) -- Subbar(e.child.sub) -- stringbar(e.arr) -- []Subbar(e.arr!) -- gets the 0 elementbar(e.arr!.sub) -- stringbar(e.arr!2) -- gets the 2nd elementbar(e.arr!2.sub) -- stringbar(e.arr[2]) -- gets the 2nd elementbar(e.arr[2].sub) -- stringbar(e.arr[e.field]) -- can use any scalar as index with [] syntax
Note that the !
operator may fail or halt:
- If it is called on an immutable collection, it will fail if the collection is shorter and has no given index; you can handle the error with try or otherwise.
- If it is called on an appendable stream, it will wait for some parallel append operation to fulfill, see Join behavior.
The !
operator can currently only be used with literal indices.
That is, !2
is valid but !x
is not valid.
To access an index with non-literal, use the brackets index: [x]
.
Assignments
Assignments (=
) could be used to give an alias to the result of an expression (e.g. literal, applied getter, math expression).
aqua
func foo(arg: bool, e: Example):-- Rename the argumenta = arg-- Assign the name b to value of e.childb = e.child-- Create a named literalc = "just string value"-- Assign the name to the result of a multiplicationd = e.field * 2
aqua
func foo(arg: bool, e: Example):-- Rename the argumenta = arg-- Assign the name b to value of e.childb = e.child-- Create a named literalc = "just string value"-- Assign the name to the result of a multiplicationd = e.field * 2
Constants
Constants are like assignments but in the root scope. They can be used in all function bodies, textually below the place of const definition. Constant values must resolve to a literal.
You can change the compilation results by overriding a constant but the override needs to be of the same type or subtype.
Constants are always UPPER_CASE
.
aqua
-- This FLAG is always trueconst FLAG = true-- This SETTING can be overwritten via CLI flagconst SETTING ?= "value"func foo(arg: string): ...func bar():-- Type of SETTING is stringfoo(SETTING)
aqua
-- This FLAG is always trueconst FLAG = true-- This SETTING can be overwritten via CLI flagconst SETTING ?= "value"func foo(arg: string): ...func bar():-- Type of SETTING is stringfoo(SETTING)
Visibility scopes
Visibility scopes follow the contracts of execution flow.
By default, everything defined textually above is available below. With some exceptions.
Functions have isolated scopes:
aqua
func foo():a = 5func bar():-- a is not defined in this function scopea = 7foo() -- a inside fo is 5
aqua
func foo():a = 5func bar():-- a is not defined in this function scopea = 7foo() -- a inside fo is 5
For loop does not export anything from it:
aqua
func foo():x = 5for y <- ys:-- Can use what was defined abovez <- bar(x)-- z is not defined in scopez = 7
aqua
func foo():x = 5for y <- ys:-- Can use what was defined abovez <- bar(x)-- z is not defined in scopez = 7
Parallel branches have no access to each other's data:
aqua
-- This will deadlock, as foo branch of execution will-- never send x to a parallel bar branchx <- foo()par y <- bar(x)-- After par is executed, all the can be usedbaz(x, y)
aqua
-- This will deadlock, as foo branch of execution will-- never send x to a parallel bar branchx <- foo()par y <- bar(x)-- After par is executed, all the can be usedbaz(x, y)
Recovery branches in conditional flow have no access to the main branch as the main branch exports values, whereas the recovery branch does not:
aqua
try:x <- foo()otherwise:-- this is not possible – will failbar(x)y <- baz()-- y is not available belowwillFail(y)
aqua
try:x <- foo()otherwise:-- this is not possible – will failbar(x)y <- baz()-- y is not available belowwillFail(y)
Streams as literals
Stream is a special data structure that allows many writes. It has a dedicated article.
To use a stream, you need to initiate it at first:
aqua
-- Initiate an (empty) appendable collection of stringsresp: *string-- Write strings to resp in parallelresp <- foo()par resp <- bar()for x <- xs:-- Write to a stream that's defined aboveresp <- baz()try:resp <- baz()otherwise:on "other peer":resp <- baz()-- Now resp can be used in place of arrays and optional values-- assume fn: []string -> ()fn(resp)-- Can call fn with empty stream: you can use it-- to construct empty values of any collection typesnilString: *stringfn(nilString)
aqua
-- Initiate an (empty) appendable collection of stringsresp: *string-- Write strings to resp in parallelresp <- foo()par resp <- bar()for x <- xs:-- Write to a stream that's defined aboveresp <- baz()try:resp <- baz()otherwise:on "other peer":resp <- baz()-- Now resp can be used in place of arrays and optional values-- assume fn: []string -> ()fn(resp)-- Can call fn with empty stream: you can use it-- to construct empty values of any collection typesnilString: *stringfn(nilString)
One of the most frequently used patterns for streams is Conditional return.
You can create a stream with Collection creation operators.