Functions and Lambdas
In Tomo, you can define functions with the func keyword:
func add(x:Int, y:Int -> Int)
return x + y
Functions require you to explicitly write out the types of each argument and the return type of the function (unless the function doesn't return any values).
For convenience, you can lump arguments with the same type together to avoid
having to retype the same type name: func add(x, y:Int -> Int)
Default Arguments
Instead of giving a type, you can provide a default argument and the type checker will infer the type of the argument from that value:
func increment(x:Int, amount=1 -> Int)
return x + amount
Default arguments are used to fill in arguments that were not provided at the callsite:
assert increment(5) == 6
assert increment(5, 10) == 15
Note: Default arguments are re-evaluated at the callsite for each function
call, so if your default argument is func foo(x=random.int(1,10) -> Int), then
each time you call the function without an x argument, it will give you a new
random number.
Keyword Arguments
Tomo supports calling functions using keyword arguments to specify the values of any argument. Keyword arguments can be at any position in the function call and are bound to arguments first, followed by binding positional arguments to any unbound arguments, in order:
func foo(x:Int, y:Text, z:Num)
return "x=$x y=$y z=$z"
func main()
assert foo(x=1, y="hi", z=2.5) == "x=1 y=hi z=2.5"
assert foo(z=2.5, 1, "hi") == "x=1 y=hi z=2.5"
As an implementation detail, all function calls are compiled to normal positional argument passing, the compiler just does the work to determine which order the arguments will be placed. Arguments are evaluated in the order in which they appear in code.
Function Caching
Tomo supports automatic function caching using the cached or cache_size=N
attributes on a function definition:
func add(x, y:Int -> Int; cached)
return x + y
Cached functions are outwardly identical to uncached functions, but internally, they maintain a table that maps a struct containing the input arguments to the return value for those arguments. The above example is functionally similar to the following code:
func _add(x, y:Int -> Int)
return x + y
struct add_args(x,y:Int)
add_cache : @{add_args: Int} = @{}
func add(x, y:Int -> Int)
args := add_args(x, y)
if cached := add_cache[args]
return cached
ret := _add(x, y)
add_cache[args] = ret
return ret
You can also set a maximum cache size, which causes a random cache entry to be evicted if the cache has reached the maximum size and needs to insert a new entry:
func doop(x:Int, y:Text, z:[Int]; cache_size=100 -> Text)
return "x=$x y=$y z=$z"
Inline Functions
Functions can also be given an inline attribute, which encourages the
compiler to inline the function when possible:
func add(x, y:Int -> Int; inline)
return x + y
This will directly translate to putting the inline keyword on the function in
the transpiled C code.
Lambdas
In Tomo, you can define lambda functions, also known as anonymous functions, like this:
fn := func(x,y:Int): x + y
The normal form of a lambda is to give a return expression after the colon, but you can also use a block that includes statements:
fn := func(x,y:Int)
if x == 0
return y
return x + y
Lambda functions must declare the types of their arguments, but do not require declaring the return type. Because lambdas cannot be recursive or corecursive (since they aren't declared with a name), it is always possible to infer the return type without much difficulty. If you do choose to declare a return type, the compiler will attempt to promote return values to that type, or give a compiler error if the return value is not compatible with the declared return type.
Closures
When declaring a lambda function, any variables that are referenced from the enclosing scope will be implicitly copied into a heap-allocated userdata structure and attached to the lambda so that it can continue to reference those values. Captured values are copied to a new location at the moment the lambda is created and will not reflect changes to local variables.
func create_adder(n:Int -> func(i:Int -> Int))
adder := func(i:Int)
return n + i
n = -1 // This does not affect the adder
return adder
func main()
add10 := create_adder(10)
assert add10(5) == 15
Under the hood, all user functions that are passed around in Tomo are passed as
a struct with two members: a function pointer and a pointer to any captured
values. When compiling the lambda to a function in C, we implicitly add a
userdata parameter and access fields on that structure when we need to access
variables from the closure. Captured variables can be modified by the lambda
function, but those changes will only be visible to that particular lambda
function.
Note: if a captured value is a pointer to a value that lives in heap memory, the pointer is copied, not the value in heap memory. This means that you can have a lambda that captures a reference to a mutable object on the heap and can modify that object. However, lambdas are not allowed to capture stack pointers and the compiler will give you an error if you attempt to do so.
1 # Functions and Lambdas6 func add(x:Int, y:Int -> Int)7 return x + y8 ```10 Functions require you to explicitly write out the types of each argument and11 the return type of the function (unless the function doesn't return any values).13 For convenience, you can lump arguments with the same type together to avoid16 ## Default Arguments18 Instead of giving a type, you can provide a default argument and the type19 checker will infer the type of the argument from that value:22 func increment(x:Int, amount=1 -> Int)23 return x + amount24 ```26 Default arguments are used to fill in arguments that were not provided at the27 callsite:30 assert increment(5) == 632 assert increment(5, 10) == 1533 ```38 random number.40 ## Keyword Arguments42 Tomo supports calling functions using keyword arguments to specify the values43 of any argument. Keyword arguments can be at any position in the function call44 and are bound to arguments first, followed by binding positional arguments to45 any unbound arguments, in order:48 func foo(x:Int, y:Text, z:Num)49 return "x=$x y=$y z=$z"51 func main()52 assert foo(x=1, y="hi", z=2.5) == "x=1 y=hi z=2.5"53 assert foo(z=2.5, 1, "hi") == "x=1 y=hi z=2.5"54 ```56 As an implementation detail, all function calls are compiled to normal57 positional argument passing, the compiler just does the work to determine which59 which they appear in code.61 ## Function Caching64 attributes on a function definition:67 func add(x, y:Int -> Int; cached)68 return x + y69 ```71 Cached functions are outwardly identical to uncached functions, but internally,72 they maintain a table that maps a struct containing the input arguments to the73 return value for those arguments. The above example is functionally similar to74 the following code:77 func _add(x, y:Int -> Int)78 return x + y80 struct add_args(x,y:Int)81 add_cache : @{add_args: Int} = @{}83 func add(x, y:Int -> Int)84 args := add_args(x, y)85 if cached := add_cache[args]86 return cached87 ret := _add(x, y)88 add_cache[args] = ret89 return ret90 ```92 You can also set a maximum cache size, which causes a random cache entry to be93 evicted if the cache has reached the maximum size and needs to insert a new94 entry:97 func doop(x:Int, y:Text, z:[Int]; cache_size=100 -> Text)98 return "x=$x y=$y z=$z"99 ```101 ## Inline Functions104 compiler to inline the function when possible:107 func add(x, y:Int -> Int; inline)108 return x + y109 ```112 the transpiled C code.114 # Lambdas116 In Tomo, you can define lambda functions, also known as anonymous functions, like117 this:120 fn := func(x,y:Int): x + y121 ```123 The normal form of a lambda is to give a return expression after the colon,124 but you can also use a block that includes statements:127 fn := func(x,y:Int)128 if x == 0129 return y130 return x + y131 ```133 Lambda functions must declare the types of their arguments, but do not require134 declaring the return type. Because lambdas cannot be recursive or corecursive135 (since they aren't declared with a name), it is always possible to infer the136 return type without much difficulty. If you do choose to declare a return type,137 the compiler will attempt to promote return values to that type, or give a138 compiler error if the return value is not compatible with the declared return139 type.141 ## Closures143 When declaring a lambda function, any variables that are referenced from the144 enclosing scope will be implicitly copied into a heap-allocated userdata145 structure and attached to the lambda so that it can continue to reference those147 is created and will not reflect changes to local variables.**150 func create_adder(n:Int -> func(i:Int -> Int))151 adder := func(i:Int)152 return n + i154 n = -1 // This does not affect the adder155 return adder157 func main()158 add10 := create_adder(10)159 assert add10(5) == 15160 ```162 Under the hood, all user functions that are passed around in Tomo are passed as163 a struct with two members: a function pointer and a pointer to any captured164 values. When compiling the lambda to a function in C, we implicitly add a167 function, but those changes will only be visible to that particular lambda168 function.171 memory, the pointer is copied, not the value in heap memory. This means that172 you can have a lambda that captures a reference to a mutable object on the heap173 and can modify that object. However, lambdas are not allowed to capture stack174 pointers and the compiler will give you an error if you attempt to do so.