code / tomo

Lines41.3K C23.7K Markdown9.7K YAML5.0K Tomo2.3K
7 others 763
Python231 Shell230 make212 INI47 Text21 SVG16 Lua6
(174 lines)

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 Lambdas
3 In Tomo, you can define functions with the `func` keyword:
5 ```tomo
6 func add(x:Int, y:Int -> Int)
7 return x + y
8 ```
10 Functions require you to explicitly write out the types of each argument and
11 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 avoid
14 having to retype the same type name: `func add(x, y:Int -> Int)`
16 ## Default Arguments
18 Instead of giving a type, you can provide a default argument and the type
19 checker will infer the type of the argument from that value:
21 ```tomo
22 func increment(x:Int, amount=1 -> Int)
23 return x + amount
24 ```
26 Default arguments are used to fill in arguments that were not provided at the
27 callsite:
29 ```tomo
30 assert increment(5) == 6
32 assert increment(5, 10) == 15
33 ```
35 **Note:** Default arguments are re-evaluated at the callsite for each function
36 call, so if your default argument is `func foo(x=random.int(1,10) -> Int)`, then
37 each time you call the function without an `x` argument, it will give you a new
38 random number.
40 ## Keyword Arguments
42 Tomo supports calling functions using keyword arguments to specify the values
43 of any argument. Keyword arguments can be at any position in the function call
44 and are bound to arguments first, followed by binding positional arguments to
45 any unbound arguments, in order:
47 ```tomo
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 normal
57 positional argument passing, the compiler just does the work to determine which
58 order the arguments will be placed. Arguments are _evaluated_ in the order in
59 which they appear in code.
61 ## Function Caching
63 Tomo supports automatic function caching using the `cached` or `cache_size=N`
64 attributes on a function definition:
66 ```tomo
67 func add(x, y:Int -> Int; cached)
68 return x + y
69 ```
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 the
73 return value for those arguments. The above example is functionally similar to
74 the following code:
76 ```tomo
77 func _add(x, y:Int -> Int)
78 return x + y
80 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 cached
87 ret := _add(x, y)
88 add_cache[args] = ret
89 return ret
90 ```
92 You can also set a maximum cache size, which causes a random cache entry to be
93 evicted if the cache has reached the maximum size and needs to insert a new
94 entry:
96 ```tomo
97 func doop(x:Int, y:Text, z:[Int]; cache_size=100 -> Text)
98 return "x=$x y=$y z=$z"
99 ```
101 ## Inline Functions
103 Functions can also be given an `inline` attribute, which encourages the
104 compiler to inline the function when possible:
106 ```tomo
107 func add(x, y:Int -> Int; inline)
108 return x + y
109 ```
111 This will directly translate to putting the `inline` keyword on the function in
112 the transpiled C code.
114 # Lambdas
116 In Tomo, you can define lambda functions, also known as anonymous functions, like
117 this:
119 ```tomo
120 fn := func(x,y:Int): x + y
121 ```
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:
126 ```tomo
127 fn := func(x,y:Int)
128 if x == 0
129 return y
130 return x + y
131 ```
133 Lambda functions must declare the types of their arguments, but do not require
134 declaring the return type. Because lambdas cannot be recursive or corecursive
135 (since they aren't declared with a name), it is always possible to infer the
136 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 a
138 compiler error if the return value is not compatible with the declared return
139 type.
141 ## Closures
143 When declaring a lambda function, any variables that are referenced from the
144 enclosing scope will be implicitly copied into a heap-allocated userdata
145 structure and attached to the lambda so that it can continue to reference those
146 values. **Captured values are copied to a new location at the moment the lambda
147 is created and will not reflect changes to local variables.**
149 ```tomo
150 func create_adder(n:Int -> func(i:Int -> Int))
151 adder := func(i:Int)
152 return n + i
154 n = -1 // This does not affect the adder
155 return adder
157 func main()
158 add10 := create_adder(10)
159 assert add10(5) == 15
160 ```
162 Under the hood, all user functions that are passed around in Tomo are passed as
163 a struct with two members: a function pointer and a pointer to any captured
164 values. When compiling the lambda to a function in C, we implicitly add a
165 `userdata` parameter and access fields on that structure when we need to access
166 variables from the closure. Captured variables _can_ be modified by the lambda
167 function, but those changes will only be visible to that particular lambda
168 function.
170 **Note:** if a captured value is a pointer to a value that lives in heap
171 memory, the pointer is copied, not the value in heap memory. This means that
172 you can have a lambda that captures a reference to a mutable object on the heap
173 and can modify that object. However, lambdas are not allowed to capture stack
174 pointers and the compiler will give you an error if you attempt to do so.