Skip to main content

Functions

Contents

Introduction

Functions are blocks of code which generally perform a specific task. They allow you to break your application down into smaller units. Rather than duplicate a block of code that is run several tines at different points within your program, you put the block into a single function and call that function whenever you need to run the code it contains. This can shorten your program, and make it easier to read and maintain.

Function Basics

Functions are declared using the function keyword, followed by a name, input parameters in brackets and then the function’s code within braces ({ and }). For example:

function areaOfCircle(radius) {
    local area = PI * radius * radius;
    return area;
}

Here, the function is called areaOfCircle. Function names may not start with a numeral. It is conventional, but not mandatory, that function names begin with a lowercase letter.

A function’s parameters are the variables into which values passed into the function when it is called are placed. These values are called arguments. Parameters are scoped to the code block within the braces. In the example above, the function areaOfCircle() has a single parameter, radius. The value of radius is used to calculate the area of a circle, which is then passed back by the function (using the return keyword) to the code that called it:

local areaOne = areaOfCircle(100);

Functions are flexible. They do not need to have any parameters (though you can’t pass data into functions without them) and don’t need to return a value. And if a function does return a value, your code doesn’t have to make use of it. The following call to the above function is perfectly legitimate, if not very useful:

areaOfCircle(100);

The function is called, and passed a value which will be put into radius, but the returned area is ignored.

Your code can ignore a function’s return value, but it can’t ignore parameters: if a function expects to receive one or more values, your code must pass those values in the call, or an exception will be thrown. At least, that’s the case if the parameters are mandatory, which is the default; it is possible to include optional parameters in a function declaration.

Even if a function lacks parameters, a statement in which it is called must still use brackets:

resetSettings();

Functions return automatically when the end the code they contain is reached. So if one of your functions doesn’t return a value, there’s no need to include a return at the end, though nothing bad will happen if you do. You will need to include return in your function code if you ever need to return early: for example, if processing can’t continue for some reason:

function processReading(reading) {
    // 'reading' is a table containing either of two keys:
    //   'error' - an error message, or
    //   'data' - a sensor reading
    if ("error" in reading) {
        server.error(reading.error);
        return null;
    }

    // Process 'reading.data'
    . . .
    return finalReading;
}

It’s good practice to check the value returned by a function (if it does indeed return a value) to confirm that the function operated correctly. In the above example, you would check the return value and if it is null, your calling code knows that an error took place and can operate accordingly (perhaps take another reading).

local result = processReading(reading);
if (result == null) restartSensor();

Declaring a function essentially adds a method to the current context object, be it the root table, or a class instance. Functions are therefore first-class objects and so may be stored in any table. They can also be placed in arrays and passed as arguments into other functions. A function may also be returned by another function. Functions can’t be serialized — please see the Developer Guide ‘Data Serialization in Squirrel’ for details.

Optional Parameters

Function parameters may be declared with default arguments. These defaults will be used if no argument is provided in the function call, rendering the parameter optional:

function areaOfCircle(radius = 10) {
    return (PI * radius * radius);
}

server.log(areaOfCircle(20));   // Displays "1256.48"
server.log(areaOfCircle());     // Displays "314.12"

This technique can be used to trap function calls that fail to pass a necessary value — without causing an exception to be thrown. Set the parameter’s default argument to null or some other suitable default and then compare the argument with that value. If the parameter has the default value, you can be reasonably sure the function was called incorrectly:

function aFunction(data = null) {
    if (data == null) {
        server.error("aFunction() called with no data");
        return;
    }

    . . .
}

Here, the code simply posts an error message to the log and returns, but depending on the need of your code, you might instead supply a default object.

Default Arguments For Reference Types

It’s important to note that the expression given for a default argument is evaluated once, when Squirrel first executes the program, and the same value is assigned to the parameter every time it’s needed. This means that if the value of the expression is a reference variable type, such as a table or an array, a reference to the same table or array is passed every time, which may not be what you expect. For example, the following function sets a default table containing two empty tables:

function aFunction(data = { "readings": {}, "timestamps: {} }) {
    . . .
}

Every time the function is called without an argument being passed in, a new data table is created that is local to the function. However, readings and timestamps are evaluated once, and so each new data table contains references to the same readings and timestamps tables.

The way around this is to re-code the function without the expression that establishes the two subsidiary tables, and conditionally initialize them within the function:

function aFunction(data = null) {
    if (data == null) {
        data = {};
        data.readings <- {};
        data.timestamps <- {};
    }

    . . .
}

Because of this, default arguments are primarily for scalar variable types: booleans, integers, floats and strings.

Capturing Unnamed Arguments

Settings a parameter’s default value is one way to make the supply of an argument optional. Another is to allow the function to collect arbitrary arguments. You do this by adding three periods as an item at the end of the list of parameters, if any. Any arguments are then made accessible through an implicit array parameter, vargv. Named parameters, which should be placed before the dots, are accessed in the usual way.

function areaOfCircle(lineColor, ...) {
    local circleColor = lineColor;
    local circleInnerRadius = vargv[0];
    local circleOuterRadius = vargv[1];
}

You do not need to included named parameters if you don’t wish to:

function areaOfCircle(...) {
    local circleColor = vargv[0];
    local circleInnerRadius = vargv[1];
    local circleOuterRadius = vargv[2];
}

This can seem an easy way of managing functions which have a large number of parameters, but you should note that this can, without care, make the code harder for a reader to understand: what is the correct order of values in the list of arguments?

Finally, you should note that you cannot call a function with arbitrary arguments if you have not included ... as a parameter in the function’s declaration.

Providing Arguments As An Array

As an alternative to passing arguments one after the other into a function call, you can also supply the arguments as a single array. The array is submitted using the function’s acall() delegate method. The array must contain sufficient elements to match the number of mandatory parameters that the function expects, or an exception will be thrown. The elements must also be in the correct order. In addition, the first element must be a context reference; typically this is this, but it need not be.

For example:

function drawCircle(center, radius, color = "BLUE") {
    server.log(center.x);
    server.log(center.y);
    server.log(radius);
    server.log(color);
}

local circleData = [this, {"x":100,"y":100}, 25];
drawCircle.acall(circleData);

will log:

100
100
25
BLUE

Function References

Many imp API methods take function references as parameters. These methods register callback functions that will be called when an event, such as a timer firing or requested data being received from a remote server, occurs. References to functions that have already been declared are passed by providing the function’s name without brackets. If you include the brackets, the function will be called and its return value set as the argument, ie. registered as the callback reference. This may be what you intend.

The following code will cause timerTriggeredFunction() to be called when the the timer fires:

function timerTriggeredFunction() {
    // Perform some work when timer fires
    server.log("Timer fired");
    return null;
}

imp.wakeup(TIMER_DURATION, timerTriggeredFunction);

but if we accidentally include a function call rather than a function reference, nothing will happen when a timer fires:

imp.wakeup(TIMER_DURATION, timerTriggeredFunction());

This is because timerTriggeredFunction() is called and returns null. Therefore null is set as the function reference expected by imp.wakeup(), ie. no function is set to be triggered when the timer fires after TIMER_DURATION seconds.

Inline Function Declarations

One way to avoid the error outlined in the previous section is to declare the argument function within (inline) the outer function call. For example, we could write the above code as:

imp.wakeup(TIMER_DURATION, function() {
    // Perform some work when timer fires
    server.log("Timer fired");
    return null;
});

Here we declare a nameless function which Squirrel passes into imp.wakeup(); the anonymous function will be called when the timer fires.

Note If an error occurs within the anonymous function, Squirrel’s error report in the impCentral log will list it as ‘unknown’.

Lambda Functions

Squirrel has a special syntax for simple, single-expression functions, known as lambdas: preface the parameter list with @ and follow it with an expression whose evaluation will be returned. For example:

local addFunc = @(a, b) a + b;
server.log(addFunc(2, 2));
// Displays '4'

Writing this in the standard function syntax would give you:

local addFunc = function(a, b) {
    return a + b;
}

This is a particularly handy way to pass one-off functions into function parameters or as return values.

Generator Functions

Generators are a special type of Squirrel function. When called, generators will establish their data structures in memory but not execute; they will not run until another part of the program includes a resume statement targeting that specific generator. When this happens, the generator begins to run until it reaches a yield statement. At this point, execution is again suspended, but the function’s internal state, including all of its local variables, is retained. The function named in the yield statement is executed. Code elsewhere in the program uses another resume to bring the function back to life once more. The generator function’s life ends when it returns, if it ever does.

Generators are not declared explicitly but implicitly, by including at least one yield statement in the body of the function. For example:

local looper;

// Define a generator function; the presence of 'yield' makes it a generator
function mainloop() {
    local count = 0;
    hardware.pin1.configure(DIGITAL_OUT);

    while (count < 10) {
        count++;
        hardware.pin1.write(1);
        yield pause(1.0);
        hardware.pin1.write(0);
        yield pause(1.0);
        server.log(count);
    }
}

function pause(duration) {
    return imp.wakeup(duration, function(){
        resume looper;
    });
}

looper = mainloop();
resume looper;

This code will toggle an LED connected to an imp001’s pin 1. The two lines at the end establish the generator and then begin its execution. Even though the yield statement causes that execution to halt (so the function pause() can run) the value of the variable count is maintained.

Generators can be a hard concept to grasp, but are a powerful tool for enabling a degree of parallelism in Squirrel code. If you’d like to learn more about generators, please see the guide ‘Squirrel Generator Functions’, which covers this topic in more depth than we can cover here.

Threads

In addition to generator functions, Squirrel provides parallel operation using threads, known as coroutines in its own terminology. However, the Electric Imp version of Squirrel does not currently support thread usage, for memory utilization reasons and because the same effect can be achieved with less complexity using generators.

Squirrel Standard Function Libraries

Squirrel maintains a number of function libraries, provided automatically without the need to import them into your code. However, not all of these are included in the Electric Imp implementation of the language, and some of those that are need to be handled in a slightly non-standard way. For instance all the functions provided by Squirrel’s math library are available to be used in device and agent code, but they must be called by prefixing their names with math. to indicate their namespace.

Squirrel’s string and blob libraries are implemented in their entirety but do not require their namespace to be included. Likewise the system library, though only the date() and time() functions are provided. Squirrel’s IO library is absent with the exception of blob streaming.

Squirrel Library Present on the imp Present on the imp but ignorable Absent from the imp
Base getroottable(), assert(), array(), type(), callee() print(), error()
(have no effect)
setroottable(), getconsttable(), setconsttable(), compilestring(), newthread(), suspend(), setdebughook(), seterrorhandler(), getstackinfos()
Blob All
IO All except for stream methods on blobs
Math All, in namespace math srand()
(ineffective)
String All
System time(), date() clock()
(use not recommended)
getenv(), system(), remove(), rename()

Function Delegate Methods

Functions have access to a handful of delegate methods to help with parameter passing and the setting of execution context.

  • acall() — See pacall(), below.
  • pacall() — Calls the target function and passes the values included in a supplied array into its parameters. The pacall() variant works just like acall(), but does not throw and error if, for example, the array lacks some of the expected parameter values.

    local array = [this, 42, "fish", "biro"];
    myFunction.acall(array);
    
  • bindenv() — Create a closure that is based on the target function and whose context object is specified by the bindenv() argument. It is used primarily to give functions that will be called asynchronously on your behalf by impOS access to For more information on context objects and the use of bindenv(), please see the guide ‘Squirrel Closures And Context Objects’. For example:

    local slack = SimpleSlack(MY_KEY);
    // 'slack' has a method, 'post()', which we bind to 'slack', and
    // supply the resulting closure to 'crashReporter' via its 'init()' method
    crashReporter.init(slack.post.bindenv(slack));
    
  • call() — See pcall(), below.

  • pcall() — Calls the target function but allows you to specify the initial (but usually hidden) context reference argument. The pcall() variant works just like call(), but does not throw and error if, for example, the you don’t pass in all of the expected parameter values.

    // In the first case, the default context is passed in as a hidden, initial argument
    myFunction(42, "fish");
    
    // In the second case, using call exposes the context reference, allowing us to pass
    // in an alternative value (in this case, 'myInstance')
    myFunction.call(myInstance, 42, "fish");
    

Back to the top