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
- Naming Conventions: Use descriptive names for variables. For app-scoped variables, consider a prefix like
global_
orapp_
even if it's redundant, for clarity. - Scope Appropriately: Only use app-scoped variables for data that truly needs to be accessible everywhere. Overusing app-scoped variables can lead to unnecessary complexity.
Variable Types
The following variable types are supported:
-
null
: Represents the absence of a value. This is case-sensitive. -
number
: Represents numeric values._
styled thousand separators are supported,1_000_000
for 1 million. -
nan
: Represents not a number. This is case-sensitive. -
inf
: Represents an infinite number. You can also have a-inf
that is a negated unary version ofinf
. -
boolean
: Represents boolean values (true
orfalse
). This is case-sensitive. -
string
: Represents textual values. There are six types of strings:-
Normal strings: These can be enclosed in single quotes (
'
), double quotes ("
), or backticks (`
). For example,'string'
,"string"
, and`string`
. -
Interpolated strings: These start with a
$
sign and can be enclosed in single quotes ('
), double quotes ("
), or backticks (`
).$'string' $"string" $`string`
Interpolated strings can contain placeholders enclosed in curly braces
{}
which will be replaced with their corresponding values during parsing. To include a literal curly brace in an interpolated string, it can be escaped with a backslash (\{
or\}
).Inside these placeholders, you can use dot paths to access nested properties within an object or array. For example, consider the following:
print($'Current $ Balance: {account.amt}!');
In this case,
account.amt
is a dot path that refers to a nested property within an object. As these use the same logic and rules for dot paths internally refer to the documentation on thedotPath()
function for more detailed usage.In both types of strings, the backslash is used as an escape character. Here are common escape sequences:
-
\\
- a literal backslash -
\n
- a newline -
\r
- a carriage return -
\t
- a tab -
\f
- a form feed (useful in console printing) -
\'
- a single quote -
\"
- a double quote
Remember, all string types support escape sequences. For example, you can include a literal quote in a string by escaping it (
\"
,\'
, or\`
for double quotes, single quotes, and backticks respectively). Similarly, a literal backslash can be included with\\
. -
-
array
: Represents an ordered collection of values. -
object
: Represents a collection of key-value pairs. -
set
: Represents a set data structure that stores unique elements. -
range
: Represents a range of numbers, created using the range() function. -
tuple
: Represents an ordered collection of immutable elements. Once a tuple is created, its contents cannot be altered. A new tuple must be created to reflect any changes. Tuples can be utilized in various ways including as 2D points, vectors, quaternions, or generic data collections.-
Creation
point2D = (10, 20) position3D = (10, 20, 30) rotation4D = (0.707, 0, 0, 0.707) genericTuple = (10, "apple", true, 5.6, "banana") largeNumericTuple = (5, 10, 15, 20, 25, 30)
-
Access: Standard indexing allows for the retrieval of tuple values. For graphic and game development purposes,
.x
,.y
,.z
, and.w
shortcuts provide direct access.x_val = point2D[1] # 10 y_val = point2D[2] # 20 z_val = position3D.z # 30
-
Special Tuple Types
- Point: Tuples with two numbers. Identifiable by the
isPoint
property. - Vector: Tuples with three numbers. Identified with the
isVec
property. - Quaternion: Tuples with four numbers. Recognizable by the
isRot
property. - General: Tuples that aren't categorized as special points, vectors, or quaternions. These tuples do not have
isPoint
,isVec
, orisRot
properties.
- Point: Tuples with two numbers. Identifiable by the
-
Benefits
- Performance: The fixed size and immutability of tuples can offer performance advantages.
- Convenience: Direct access shortcuts and structured data enhance code readability and ease of use.
- Flexibility: Tuples can serve diverse purposes without necessitating distinct data structures.
-
-
error
: Represents an error or exception that occurs during the execution of a formula. An error object typically contains information about the error, such as a message, error type, error code, and any additional information that might be relevant. -
function
: Represents a user-defined function. Functions are reusable code that perform specific tasks. -
class
: Represents a user-defined class. Classes are blueprints for creating objects. -
instance
: Represents an instance of a user-defined class. Instances are individual objects created from a class blueprint. -
dotPath
: Represents a dot-separated string with optional index accessors. It is created using thedotPath()
function and is used to access nested properties within an object or array. For detailed usage and examples, refer to the documentation on thedotPath()
function. -
hardReference
: Represents a strong reference to certain types like objects, arrays, and others. These are created by setting a variable to a referenceable object, and are automatically created. -
weakReference
: Represents a weak reference to a data structure or entity. Unlike strong references, a weak reference doesn't prevent its target from being garbage collected. This provides a mechanism to reference certain types like objects, arrays, and others without preventing them from being cleaned up when they're no longer needed. These are not automatically followed like a strong reference. Therefore, you must use.exists
and.value
similar to accessing an object. -
matchType
: Represents the type of something. UsematchType('number')
to create this reference. This is used forensureToMatch
for example.
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:
- Accessing the first element of an array:
setLocal(colors, ["red", "blue", "green"]);
print(colors[1]); // Output: "red"
- Accessing the first character of a string:
setLocal(name, "Flowata");
print(name[1]); // Output: "F"
- 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:
-
Accessing every second element of an array:
setLocal(myArray, [10, 20, 30, 40, 50, 60]); print(myArray[1:6:2]); // Output: [10, 30, 50]
-
Accessing elements in reverse order:
setLocal(myArray, [10, 20, 30, 40, 50]); print(myArray[5:1:-1]); // Output: [50, 40, 30, 20, 10]
-
Accessing elements with a negative start and end:
setLocal(myArray, [10, 20, 30, 40, 50]); print(myArray[-1:-5:-2]); // Output: [50, 30]
-
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.
-
Initializing an empty array:
setLocal(myArray, []);
-
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]
-
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]
-
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]
-
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]
-
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:
-
Replacement requires an array of values that matches the length of the subset you're targeting. It's a one-to-one mapping where each value in the subset is replaced by a corresponding value from the provided array.
-
Broadcasting involves taking a single value and replicating it across a specified range in the array. It's a one-to-many mapping where one value is used to replace multiple values in the subset.
-
If there's a mismatch between the range's length and the provided array's length during a replacement operation, Flowata will throw an error. Ensure that the lengths match to avoid any issues.
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:
- Accessing the last element of an array:
setLocal(fruits, ["apple", "banana", "cherry"]);
print(fruits[-1]); // Output: "cherry"
- Accessing the last character of a string:
setLocal(word, "hello");
print(word[-1]); // Output: "o"
- 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:
- For strings, use the
substring
function. - For arrays or objects, use
dotPath
to navigate as deep as possible, and then use a regular index accessor that isn't part of the dotPath string, or property access to retrieve the desired value. Another option would be to precompute the raw direct path.
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
-
Basic Usage
setLocal(myObject, { key1: ["item1", "item2", "item3"] }); setLocal(dotPath("myObject.key1[1]"), "newItem"); print(myObject.key1[1]); // Output: "newItem"
-
Escaping Special Characters
setLocal(myObject, { "key1.key3": ["item1", "item2"] }); setLocal(dotPath("myObject.key1\.key3[1]"), "newItem"); print(myObject["key1.key3"][1]); // Output: "newItem"
-
Using Ranges
setLocal(myArray, [1, 2, 3, 4, 5]); print(dotPath("myArray[2:4]")); // Output: [2, 3, 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]
-
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.
message
: A descriptive message about the error.isRuntimeError
: Istrue
when it was created by the runtime (interpreter, memory manager, etc), otherwise `false``.isUserCreated
: Opposite ofisRuntimeError
. Istrue
when you create your own custom errors. It isfalse
when it was created by the runtime (interpreter, memory manager, etc)errorType
: A string representing the type or category of the error.errorCode
: A string or numeric code associated with the error.additionalInfo
: An optional object containing any additional details or context about the error.
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:
-
Basic Usage:
setLocal(myVar, 123); print(varInfo(myVar)); // Output: { type: "number", value: 123 }
-
For Grouped Variables:
setLocal(myGroupedVar, (123)); print(varInfo(myGroupedVar)); // Output: { type: "number", value: 123 } setLocal(myEmptyGroupedVar, ()); print(varInfo(myGroupedVar)); // Output: { type: "null" }
-
Example for Range:
setLocal(myRange, range(5)); print(varInfo(myRange)); // Output: { type: "range", start: 1, end: 5 }
-
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:
-
Using the
type()
Function:print(type(123)); // Output: "number"
-
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:
.exists
: A boolean value indicating if the referenced object still exists in memory..value
: Retrieves the actual object if it exists, or returnsnull
if it has been garbage collected.
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:
object
array
set
tuple
instance
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.