Variables

Introduction to Variables in Flowata

In programming and scripting, a variable is akin to a storage box where you can keep data values. Think of it as a labeled container where you can store a piece of information, retrieve it, or change its contents. Variables allow you to store, manipulate, and reference data throughout your code, making them fundamental to any computational process.

In our low-code language, variables are not just mere storage units; they come with specific scopes and types that dictate their behavior and accessibility. Understanding these scopes and types is crucial to effectively use variables and ensure data integrity in your applications.

Understanding Variable Scope

The "scope" of a variable refers to the part of the code where a variable can be accessed or modified. Our language provides three distinct scopes for variables: local, screen, and app. Each scope serves a specific purpose, ensuring that variables are used appropriately and efficiently.

Local Variables

Local variables can be defined individually or in bulk using the setLocal function. When defined individually, they are temporary and only exist within the formula where they are defined. When defined in bulk using an object, each key-value pair in the object becomes a local variable.

setLocal(varName, value) or setLocal(object)

Examples:

// Setting a single variable
setLocal(local.myVar, 10);
print(local.myVar); // Output: 10

// Setting multiple variables using an object
setLocal({ apples: 1, oranges: 4, pears: 2 });
print(local.apples); // Output: 1
print(local.oranges); // Output: 4

App-Scoped Variables

App-scoped variables can be defined individually or in bulk using the setApp function. They are accessible across different screens and can store app-level state.

setApp(varName, value) or setApp(object)

Examples:

// Setting a single variable
setApp(myVar, "Hello, world!");
print(app.myVar); // Output: Hello, world!

// Setting multiple variables using an object
setApp({ theme: "dark", language: "English" });
print(app.theme); // Output: dark

Screen-Scoped Variables

Screen-scoped variables can be defined individually or in bulk using the setScreen function. They are accessible only within the current screen and can store screen-level state.

setScreen(varName, value) or setScreen(object)

Examples:

// Setting a single variable
setScreen(count, 5);
print(screen.count); // Output: 5

// Setting multiple variables using an object
setScreen({ username: "JohnDoe", age: 25 });
print(screen.username); // Output: JohnDoe

Non-prefixed Variable Access

For convenience, you can access a variable without a prefix. The system will resolve the variable in the following order: local, screen, then app.

setLocal(local.color, "blue");
setScreen(color, "red");
print(color); // Output: blue (since local scope is checked first)

Note: While non-prefixed access can be convenient, it can also lead to potential confusion if variables with the same name exist in multiple scopes. It's recommended to use prefixes for clarity, especially in complex applications.

Best Practices

Variable Types

The following variable types are supported:

Complex Data Types Helper Functions

Some data types have helper functions to make working with them easier, such as array, object, and set.

Arrays

Arrays are ordered collections of values. Each value in an array is associated with an index, which is an integer starting from one for the first element.

Array Creation

To create an array, you use square brackets [] with values separated by commas ,.

setLocal(myArray, [1, 2, 3]);
print(myArray); // Output: [1, 2, 3]

Array Access

To access a value in an array, you use the variable name followed by the index in square brackets [].

setLocal(myArray, ["apple", "banana", "cherry"]);
print(myArray[2]); // Output: "banana"

If you try to access an index that is not present in the array, the result will be null.

setLocal(myArray, ["apple", "banana", "cherry"]);
print(myArray[5]); // Output: null

Index Accessors

Index accessors are a powerful tool in Flowata that allow you to directly access specific elements within arrays, objects, and other data structures. Unlike the dotPath function, when using index accessors directly, you can utilize variables and string keys without any special formatting.

You can use [n] style index accessors after the declaration of an array, object, function result to get a result. This also works on strings to access a specific character. 1-based indexing is used, instead of 0-based index because humans count from 1. Off-by-one and other errors can occur because of this. Due to the language being interpreted, human programmers caring about memory address spacing is irrelevant in this environment. If you do happen to access using a zero-based index, it will return null.

Examples:

  1. Accessing the first element of an array:
setLocal(colors, ["red", "blue", "green"]);
print(colors[1]); // Output: "red"
  1. Accessing the first character of a string:
setLocal(name, "Flowata");
print(name[1]); // Output: "F"
  1. Accessing an element using a zero-based index:
setLocal(animals, ["cat", "dog", "bird"]);
print(animals[0]); // Output: null

Using Index Accessors with Objects

For objects, you can access properties using their keys:

setLocal(myObject, {name: "Alice", age: 30});
print(myObject["name"]); // Output: Alice

If the key is stored in a variable, you can use it directly:

setLocal(key, "age");
print(myObject[key]); // Output: 30

Accessor Ranges

Accessor ranges are a powerful feature in Flowata that allows you to access or modify a subset of an array. The syntax for an accessor range is [start:end], where both start and end are inclusive. Flowata uses 1-based indexing, so the first item is accessed with 1 instead of 0.

Retrieving Values Using Accessor Ranges

To retrieve a subset of an array, you can use the direct index accessor with the range syntax:

setLocal(myArray, [10, 20, 30, 40, 50]);
print(myArray[2:4]); // Output: [20, 30, 40]

Setting Values Using Accessor Ranges

To set or replace a subset of an array, you can use the direct index accessor with the range syntax followed by the assignment:

setLocal(myArray, [10, 20, 30, 40, 50]);

// rewrite element 2,3,4 in the array
setLocal(myArray[2:4], [100, 200, 300]);

print(myArray); // Output: [10, 100, 200, 300, 50]

In the example above, the subset of the array from index 2 to 4 is replaced with the new values [100, 200, 300].

Note: When setting values using an accessor range, both the start and end of the range must be specified. Partial ranges like myArray[2:] or myArray[:4] are disallowed for setting values but can be used for reading values.

Note: Using an index accessor immediately after specifying an accessor range is not supported. For example, myArray[2:4][1] would be invalid. This is because accessing a specific index within a range can be ambiguous and lead to unintended behavior.

By using direct index accessors with accessor ranges, you can efficiently manipulate specific portions of arrays in Flowata.

Accessor Ranges with Steps

Accessor ranges in Flowata can also include a step within a range. The syntax for an accessor range with a step is [start:end:step], where both start and end are inclusive. The step determines the increment between each value in the range.

If the step is positive, the sequence will be increasing, and if the step is negative, the sequence will be decreasing. If the step is omitted, it defaults to 1.

Examples:

  1. Accessing every second element of an array:

    setLocal(myArray, [10, 20, 30, 40, 50, 60]);
    print(myArray[1:6:2]); // Output: [10, 30, 50]
  2. Accessing elements in reverse order:

    setLocal(myArray, [10, 20, 30, 40, 50]);
    print(myArray[5:1:-1]); // Output: [50, 40, 30, 20, 10]
  3. Accessing elements with a negative start and end:

    setLocal(myArray, [10, 20, 30, 40, 50]);
    print(myArray[-1:-5:-2]); // Output: [50, 30]
  4. Setting values in reverse order using a negative step:

    setLocal(myArray, []);
    setLocal(myArray[5:7], [10, 20, 30]);
    print(myArray); // Output: [null, null, null, null, 10, 20, 30]

Note: When setting values using an accessor range with a step, both the start and end of the range must be specified. Partial ranges like myArray[2::2] or myArray[:4:2] are disallowed for setting values but can be used for reading values.

Note: Using an index accessor immediately after specifying a range with a step is not supported. This is because accessing a specific index within a range can be ambiguous and lead to unintended behavior. For example, setLocal(myArray[2:4:2][1], [10, 20, 30]); would be invalid.

Using Accessor Ranges with Steps as a Fill Operation with Single Value Broadcasting

In Flowata, when working with arrays, you can utilize accessor ranges to either replace specific subsets of the array or broadcast a single value across a range. The distinction between these two operations is determined by the type of value provided for the replacement: an array indicates a direct replacement, while a single value indicates broadcasting.

  1. Initializing an empty array:

    setLocal(myArray, []);
  2. Filling the array with 10 zero values (Broadcasting):

    setLocal(myArray[1:10], 0);
    print(myArray); // Output: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  3. Setting every other value to 1 (Broadcasting):

    setLocal(myArray[1:10:2], 1);
    print(myArray); // Output: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
  4. Setting alternating values to 2 (Broadcasting):

    setLocal(myArray[2:10:2], 2);
    print(myArray); // Output: [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
  5. Replacing the first and last values with 0 (Direct Replacement):

    setLocal(myArray[1], 0);
    setLocal(myArray[10], 0);
    print(myArray); // Output: [0, 2, 1, 2, 1, 2, 1, 2, 1, 0]
  6. Replacing specific indices with an array of repeated values (Replacement):

    setLocal(myArray[4:6], [3, 3, 3]);
    print(myArray); // Output: [0, 2, 1, 3, 3, 3, 1, 2, 1, 0]

Important Notes:

Expansion Behavior

Expansion Behavior: If you try to set a value in a position that doesn't exist, Flowata will automatically expand the array, filling in any gaps with a default value of null.

setLocal(myArray, []);
setLocal(myArray[5:7], [10, 20, 30]);
print(myArray); // Output: [null, null, null, null, 10, 20, 30]

Negative Index Accessors

In Flowata, you can use negative indexing to access elements from the end of an array, object, or string.

When you use a negative index, it counts from the end of the sequence. For instance, an index of -1 refers to the last element, -2 refers to the second last, and so on. This feature can be especially useful when you want to access the last few elements without knowing its exact length.

Examples:

  1. Accessing the last element of an array:
setLocal(fruits, ["apple", "banana", "cherry"]);
print(fruits[-1]); // Output: "cherry"
  1. Accessing the last character of a string:
setLocal(word, "hello");
print(word[-1]); // Output: "o"
  1. Accessing the second last element of an array:
setLocal(numbers, [10, 20, 30, 40]);
print(myArray[-1]); // Output: 40
print(numbers[-2]); // Output: 30

dotPath(path)

Converts a dot-separated string with optional index accessors and ranges into a format that can be used to access nested properties within an object or array. This function is particularly useful when generating paths to nested properties programmatically or when using variables for paths.

If your path contains a dot, colon, brackets, or underscores that should not be interpreted as dot separators or thousand separators (for example, if a key in your object contains actual dots, colons, brackets, or underscores), you can escape them using a backslash (\. or \:, \[ or \], \_ respectively).

Underscores (_) within index accessors are interpreted as thousand separators. For instance, an accessor like [1_000] is interpreted as accessing the 1000th index.

The dotPath() function also addresses whitespace within keys by removing leading, trailing, and intermediate spaces from each key. Consecutive dots are treated as a single dot, effectively bypassing them.

Special Considerations for Ranges and Index Accessors

Note: Ranges and index accessors in dotPath only support numbers directly. If you need to use a variable for the range or index:

Note: When setting values using a range, both the start and end of the range must be specified. Partial ranges like myArray[2:] or myArray[:4] are disallowed for setting values but can be used for reading values.

Note: Using an index accessor immediately after specifying a range is not supported. This is because accessing a specific index within a range can be ambiguous and lead to unintended behavior. For example, setLocal(dotPath("myArray[2:4][1]"), [10, 20, 30]); would be invalid.

Examples

  1. Basic Usage

    setLocal(myObject, { key1: ["item1", "item2", "item3"] });
    setLocal(dotPath("myObject.key1[1]"), "newItem");
    
    print(myObject.key1[1]); // Output: "newItem"
  2. Escaping Special Characters

    setLocal(myObject, { "key1.key3": ["item1", "item2"] });
    setLocal(dotPath("myObject.key1\.key3[1]"), "newItem");
    
    print(myObject["key1.key3"][1]); // Output: "newItem"
  3. Using Ranges

    setLocal(myArray, [1, 2, 3, 4, 5]);
    print(dotPath("myArray[2:4]")); // Output: [2, 3, 4]
  4. Setting Values Using Ranges

    setLocal(myArray, [1, 2, 3, 4, 5]);
    setLocal(dotPath("myArray[2:4]"), [10, 20, 30]);
    
    print(myArray); // Output: [1, 10, 20, 30, 5]
  5. Using Steps with Ranges in dotPath

    setLocal(myArray, [1, 2, 3, 4, 5, 6]);
    setLocal(dotPath("myArray[1:6:2]"), [7, 8, 9]);
    
    print(myArray); // Output: [7, 2, 8, 4, 9, 6]

escapeDotPath(path)

A utility function that escapes all dots, colons, underscores and brackets in a given path string. This is especially useful when working with object keys that contain dots, colons, underscores or brackets. It's also crucial for sanitizing user input to prevent potential object or property injection attacks.

Example:

setLocal(escapedPath, escapeDotPath("key1.key3[1:3]"));
setLocal(myObject, { "key1.key3[1:3]": "item2" });
setLocal(dotPath(escapedPath), "newItem");

print(myObject["key1.key3[1:3]"]); // Output: "newItem"

Note: Always sanitize user input, especially when it's used to access or modify data structures, to ensure the security and integrity of your application.

Note: The dotPath() function and escapeDotPath() are utilities to generate the correct path format. They don't perform any operations on the object or array themselves.

Range

Ranges are used to generate sequences of numbers that can be utilized for various purposes, including indexing and looping. They are created using the `range`` function and provide a convenient way to define numeric sequences.

range(end)

Generates a sequence of numbers from 1 up to (but not including) the specified end.

range(start, end)

Generates a sequence of numbers from the start up to (but not including) the specified end.

range(start, end, step)

Generates a sequence of numbers from the start up to (but not including) the end, incrementing by the step.

Examples:

print(range(5)); // Output: [1, 2, 3, 4]
print(range(2, 5)); // Output: [2, 3, 4]
print(range(2, 8, 2)); // Output: [2, 4, 6]

Usage as an Index Accessor:

The range function can also be used as an index accessor, providing a more intuitive way to generate index sequences for arrays and objects.

setLocal(myArray, [10, 20, 30, 40, 50]);

print(myArray[range(2, 4)]); // Output: [20, 30, 40]
print(myArray[range(1, 5, 2)]); // Output: [10, 30, 50]
Example with a Local Variable:

You can assign a range to a local variable and then use it as an index accessor.

setLocal(myArray, [10, 20, 30, 40, 50]);
setLocal(myRange, range(2, 4));

print(myArray[myRange]); // Output: [20, 30, 40]

In this example, the myRange variable is assigned the range range(2, 4), which generates a sequence of indices from 2 to 4. The myArray[myRange] statement then accesses the values at those indices in the myArray array.

Usage in a Loop:

Ranges can be particularly useful in loops to iterate over a sequence of numbers.

Examples:

for (range(6), seq(
   setLocal(number, $value),
   print(number)
)); // Output: 0 1 2 3 4 5

In this example, the range(6) generates a sequence of numbers from 1 to 6, and the loop iterates through each number, setting the number variable and printing its value.

Immutability

Once a range is created, it is immutable and cannot be modified. If you need to create a different range, you'll need to create a new one using the range function.

Error

Errors in Flowata are represented as special objects that encapsulate information about an exception or unexpected event that occurs during the execution of a formula. These error objects can be created manually or might be returned by certain functions when they encounter issues.

error(message, errorType, errorCode, additionalInfo)

Creates an error object with the specified properties.

Example:

setLocal(myError, error("Invalid Input", "InputError", 4001, { "inputValue": "abc123" }));
print(myError); // Output: { message: "Invalid Input", errorType: "InputError", errorCode: 4001, additionalInfo: { "inputValue": "abc123" } }

Variable Information Function

varInfo(variable)

Returns information about the given variable. This can include its type and, for specific types like errors and ranges, additional details.

For variables enclosed in additional parentheses "grouped", their type and value are determined by what's inside the parentheses. If the parentheses are empty, the variable is treated as null.

Examples:

  1. Basic Usage:

    setLocal(myVar, 123);
    print(varInfo(myVar)); // Output: { type: "number", value: 123 }
  2. For Grouped Variables:

    setLocal(myGroupedVar, (123));
    print(varInfo(myGroupedVar)); // Output: { type: "number", value: 123 }
    
    setLocal(myEmptyGroupedVar, ());
    print(varInfo(myGroupedVar)); // Output: { type: "null" }
  3. Example for Range:

    setLocal(myRange, range(5));
    print(varInfo(myRange)); // Output: { type: "range", start: 1, end: 5 }
  4. Example for Error:

    setLocal(myError, error("Invalid Input", "InputError", 4001, { "inputValue": "abc123" }));
    print(varInfo(myError)); // Output: { type: "error", message: "Invalid Input", errorType: "InputError", errorCode: 4001, additionalInfo: { "inputValue": "abc123" } }

type(variable)

Returns a string representing the type of the given variable. This function provides a simpler way to get the type information as compared to using the varInfo() function.

Examples:

  1. Using the type() Function:

    print(type(123)); // Output: "number"
  2. Using the type() Function for Grouped Variable:

    print(type((123))); // Output: "number"

WeakReference Usage

The WeakReference type is introduced as a safeguard against potential circular references. Given that the language uses reference counting for memory management, circular references are problematic and are disallowed.

Using a WeakReference ensures that an object can be referenced without forming a strong link that could prevent its memory from being reclaimed. This is particularly helpful in scenarios where bi-directional or potential cyclic relationships might be established.

Typical Use Case: One common usage pattern for WeakReference is in hierarchical relationships. For instance, when an object (like a 'child' or 'member') is passed a reference to another object (like a 'parent' or 'owner'), it's recommended to use a WeakReference for the parent. This ensures that the child can access its parent without preventing the parent from being garbage collected if all other references to it are removed.

API:

Applicable Types:

It's essential to note that not all variable types can be weakly referenced. The WeakReference type is applicable to complex types that can form or hold references and where circular references might be a concern. These types include:

For fundamental types like number, string, boolean, and others, using WeakReference is not applicable or necessary.

By employing weak references in scenarios like parent-child relationships and structures that can form or hold references, developers can create interconnected objects without inadvertently introducing memory leaks due to lingering strong references.

Creating a WeakReference:

To create a WeakReference to an object, you can use the weakReference() function by passing in the normal hard reference. Here's an example of creating and using a WeakReference object:

// Creating a weakReference to an object
setLocal(myObject, { name: "John" });
setLocal(weakRef, weakReference(myObject));

// Checking if the referenced object still exists
print(if(weakRef.exists, weakRef.value.name, "The referenced object has been garbage collected."));

In this example, weakReference() takes an object (myObject) and returns a WeakReference to that object. You can then use the exists property of the WeakReference to check if the referenced object still exists and, if so, access its properties.