Functions 2

Immutable function args by default

In V function arguments are immutable by default, and mutable args have to be marked on call.

Since there are also no globals, that means that the return values of the functions, are a function of their arguments only, and their evaluation has no side effects (unless the function uses I/O).

Function arguments are immutable by default, even when references are passed. An array returned from an immutable argument remains immutable, including when returned through a local function value, a narrowed if or match branch, or after an exiting if guard. Use .clone() for a mutable copy.

A pointer returned through a callback can still refer to an immutable argument, even if the callback returns voidptr. Converting that result to a typed reference does not make the underlying object mutable. Different pointee types do not prove separate storage: a voidptr can erase the type of an existing reference. For an opaque container lookup that guarantees separate mutable component storage, place the conversion in unsafe { ... }. The caller must ensure the returned pointer does not provide mutable access to an immutable argument.

When a readable function returns a stored pointer to separate storage, or newly allocated storage that contains no references or other shared storage from its arguments, the returned reference does not borrow the containing object or the function's other arguments. A new outer object can still borrow an argument through a reference-bearing field, such as &Box{item: item}.

A scalar passed by value to a callback is independent of the caller's storage. If a callback parameter is a reference, an implicitly referenced scalar remains borrowed from its immutable argument; the scalar's expression type alone does not establish a by-value copy. Scalar fields supplied with collapsed struct argument syntax and scalar elements decomposed into by-value parameters are copied too. Pointer fields and elements can still share storage.

[!NOTE] However, V is not a purely functional language.

There is a compiler flag to enable global variables (-enable-globals), but this is intended for low-level applications like kernels and drivers.

Mutable arguments

It is possible to modify function arguments by declaring them with the keyword mut:

A method with a mut receiver requires a mutable value receiver even when it only changes private fields in another module. An immutable value parameter cannot call such a method: mutations would affect its copy and be lost when the function returns. Declare the parameter or receiver with mut when its changes must reach the caller.

An explicit mut param &T takes mutable pointer storage. Passing mut &value creates temporary pointer storage for that call, including when value is a mut value T parameter. Changes to fields reach value; rebinding the temporary pointer does not rebind the caller's value.

struct User { name string mut: is_registered bool } fn (mut u User) register() { u.is_registered = true } mut user := User{} println(user.is_registered) // "false" user.register() println(user.is_registered) // "true"

In this example, the receiver (which is just the first argument) is explicitly marked as mutable, so register() can change the user object. The same works with non-receiver arguments:

fn multiply_by_2(mut arr []int) { for i in 0 .. arr.len { arr[i] *= 2 } } mut nums := [1, 2, 3] multiply_by_2(mut nums) println(nums) // "[2, 4, 6]"

Note that you have to add mut before nums when calling this function. This makes it clear that the function being called will modify the value.

A comptime app.$method(...) call also requires mut when passing a pointer variable to an explicit mut param &T parameter.

It is preferable to return values instead of modifying arguments, e.g. user = register(user) (or user.register()) instead of register(mut user). Modifying arguments should only be done in performance-critical parts of your application to reduce allocations and copying.

For this reason V doesn't allow the modification of arguments with primitive types (e.g. integers). Only more complex types such as arrays and maps may be modified.

Variable number of arguments

V supports functions that receive an arbitrary, variable amounts of arguments, denoted with the ... prefix. Below, a ...int refers to an arbitrary amount of parameters that will be collected into an array named a.

Methods on generic structs can also accept these arguments. The receiver type determines the specialization, and the arguments are collected into the parameter array. The element type keeps its declaring module when the method is called from another module.

fn sum(a ...int) int { mut total := 0 for x in a { total += x } return total } println(sum()) // 0 println(sum(1)) // 1 println(sum(2, 3)) // 5 // using array decomposition a := [2, 3, 4] println(sum(...a)) // <-- using prefix ... here. output: 9 b := [5, 6, 7] println(sum(...b)) // output: 18

Anonymous & higher order functions

Calling a function parameter or local function variable uses that binding, even when another module defines a function constant with the same name.

fn sqr(n int) int { return n * n } fn cube(n int) int { return n * n * n } fn run(value int, op fn (int) int) int { return op(value) } fn main() { // Functions can be passed to other functions println(run(5, sqr)) // "25" // Anonymous functions can be declared inside other functions: double_fn := fn (n int) int { return n + n } println(run(5, double_fn)) // "10" // Functions can be passed around without assigning them to variables: res := run(5, fn (n int) int { return n + n }) println(res) // "10" // You can even have an array/map of functions: fns := [sqr, cube] println(fns[0](10)) // "100" fns_map := { 'sqr': sqr 'cube': cube } println(fns_map['cube'](2)) // "8" }

Lambda expressions

Lambda expressions in V are small anonymous functions, defined using the |variables| expression syntax. Note: this syntax is valid only inside calls to higher order functions.

Here are some examples:

mut a := [1, 2, 3] a.sort(|x, y| x > y) // sorts the array, defining the comparator with a lambda expression println(a.map(|x| x * 10)) // prints [30, 20, 10]
// Lambda function can be used as callback fn f(cb fn (a int) int) int { return cb(10) } println(f(|x| x + 4)) // prints 14

Function values passed to generic methods are checked by their parameter and return types. Parameter names and whitespace do not affect function type compatibility.

Closures

An instance method can be used as an unbound function value through its type, such as App.method. Its first parameter is the receiver, including the receiver's & or mut modifier. This form captures no receiver and creates no closure. The same applies to T.$method inside a compile-time loop over T.methods.

Callbacks in specialized generic functions retain the functions they call, including imported functions referenced only from the callback body.

V supports closures too. This means that anonymous functions can inherit variables from the scope they were created in. They must do so explicitly by listing all variables that are inherited.

my_int := 1 my_closure := fn [my_int] () { println(my_int) } my_closure() // prints 1

Inherited variables are copied when the anonymous function is created. This means that if the original variable is modified after the creation of the function, the modification won't be reflected in the function.

mut i := 1 func := fn [i] () int { return i } println(func() == 1) // true i = 123 println(func() == 1) // still true

However, the captured copy can be modified inside the anonymous function. mut only makes the closure's copy mutable; it does not capture the original variable by reference. The change won't be reflected outside, but will be in later function calls.

This also applies to captured arrays, maps, and structs. For example, fn [mut files] inside os.walk(...) will modify the closure's files copy, not the outer files variable.

fn new_counter() fn () int { mut i := 0 return fn [mut i] () int { i++ return i } } c := new_counter() println(c()) // 1 println(c()) // 2 println(c()) // 3

A callback's captured values remain available while the callback is stored in a struct field, including when that field is assigned through a pointer to the struct.

If you need the value to be modified outside the function, use a reference. Capturing a mut parameter preserves its reference to the caller's value, including when the closure passes it to a spawned function.

mut i := 0 mut ref := &i print_counter := fn [ref] () { println(*ref) } print_counter() // 0 i = 10 print_counter() // 10

For callbacks like os.walk, use os.walk_with_context when the callback needs to update caller-owned state:

import os mut files := []string{} os.walk_with_context('.', &files, fn (mut files []string, file string) { files << file }) println(files.len > 0)

Parameter evaluation order

The evaluation order of the parameters of function calls is NOT guaranteed. Take for example the following program:

fn f(a1 int, a2 int, a3 int) { dump(a1 + a2 + a3) } fn main() { f(dump(100), dump(200), dump(300)) }

V currently does not guarantee that it will print 100, 200, 300 in that order. The only guarantee is that 600 (from the body of f) will be printed after all of them.

This may change in V 1.0 .