Quick procedures
With polymorphic variables and #auto
, it is possible to write a completely type-generic procedure in Onyx.
print_iterator :: (msg: $T, iterable: $I) -> #auto {
println(msg);
for iterable {
println(it);
}
return 1234;
}
print_iterator("List:", u32.[ 1, 2, 3, 4 ]);
print_iterator(8675309, 5 .. 10);
No types are given in the procedure above. msg
and iterable
can be any type, provided that iterable
can be iterated over using a for
loop. This kind of procedure, one with no type information, is given a special shorthand syntax.
print_iterator :: (msg, iterable) => {
println(msg);
for iterable {
println(it);
}
return 1234;
}
print_iterator("List:", u32.[ 1, 2, 3, 4 ]);
print_iterator(8675309, 5 .. 10);
Here the =>
signifies that this is a quick procedure. The types of the parameters are left out, and can take on whatever value is provided. Programming using quick procedures feels more like programming in JavaScript or Python, so don't abuse them. They are very useful when passing procedures to other procedures.
map :: (x: $T, f: (T) -> T) -> T {
return f(x);
}
// Note that the paraentheses are optional if
// there is only one parameter.
y := map(5, value => value + 4);
println(y);
You can also have a mix between quick procedures and normal procedures. This examples shows an alternative way of writing -> #auto
.
// The => could be -> #auto, or -> T.
find_smallest :: (items: [] $T) => {
small := items[0];
for items {
if it < small do small = it;
}
return small;
}
println(find_smallest(u32.[6,2,5,1,10]));
Closures
Onyx has experimental support for closures when using quick-procedures. Currently, this is in the form of explicit closure, where every captured variable has to be declared before it can be used. This restriction will likely be lifted in the future when other internal details are figured out.
To declare a closure, simply add a closure block to the quick procedure definition.
main :: () {
x := 10
// Here, x is captured by value, and a copy is made for
// this quick procedure.
f := ([x] y: i32) => {
return y + x
}
f(20) |> println(); // Prints 30
}
Captured values can either by value, or by pointer.
To capture by pointer, simply place a &
in front of the variable name.
main :: () {
x := 10
// Here, x is captured by pointer
f := ([&x] y) => {
*x = 20
return y + *x
}
f(20) |> println(); // Prints 40
println(x); // Prints 20
}
Currying
A form of function currying is possible in Onyx using chained quick procedures, and passing previous arguments to each subsequent quick procedure.
add :: (x: i32) => ([x] y: i32) => ([x, y] z: i32) => {
return x + y + z
}
main :: () {
partial_sum := add(1)(2)
sum1 := partial_sum(3)
sum2 := partial_sum(10)
println(sum1)
println(sum2)
}
Internal details of Closures
Every time a closure is encountered at runtime, a memory allocation must be made
to accommodate the memory needed to store the captured values. To do this, a
builtin procedure called __closure_block_allocate
is called. This procedure is
implemented by default to invoke context.closure_allocate
. By default, context.closure_allocate
allocates a buffer from the temporary allocator. If you want to change
how closures are allocated, you can change this procedure pointer to do something
different.
main :: () {
context.closure_allocate = (size: i32) -> rawptr {
printf("Allocating {} bytes for closure.\n", size)
// Allocate out of the heap
return context.allocator->alloc(size)
}
x := 10
f := ([x] y: i32) => {
return y + x
}
f(20) |> println() // Prints 30
}