aboutsummaryrefslogtreecommitdiff
path: root/docs/functions.md
blob: 174ad23acbeb03f2299ee0a24d37167e2540cd49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# Functions and Lambdas

In Tomo, you can define functions with the `func` keyword:

```tomo
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:

```tomo
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:

```tomo
>> increment(5)
= 6

>> 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=Int.random(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:

```tomo
func foo(x:Int, y:Text, z:Num):
    return "x=$x y=$y z=$z"

>> foo(x=1, y="hi", z=2.5)
= "x=1 y=hi z=2.5"

>> 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:

```tomo
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:

```tomo
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 add_cache:has(args):
        return add_cache:get(args)
    ret := _add(x, y)
    add_cache:set(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:

```tomo
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:

```tomo
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:

```tomo
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:

```tomo
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.**

```tomo
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
...
add10 := create_adder(10)
>> 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.