An array is a data structure consisting of a collection of elements.
Almost every programming language allows you to represent different types of collections. In BoxLang, we have three types of collections: arrays, structures, and queries.
An array is a number-indexed list. Imagine you had a blank piece of paper and drew a set of three small boxes in a line:
--- --- ---
| || || |
--- --- ---
You could number each one by its position from left to right:
We have a three-element Array. BoxLang arrays can grow and shrink dynamically at runtime, just like Array Lists or Vectors in Java, so if we added an element, it’d usually go on the end or be appended at the end.
If you asked the array for the element in position two, you’d get back Lunch. Ask for the last element, and you’ll get back: Dessert.
The Story of One
Now, have you detected something funny with the ordering of the elements? Come on, look closer....... They start with 1 and not 0, now isn't that funny. BoxLang is one of the few languages where array indexes start at 1 and not 0. So if you have a PHP, Ruby, or Java background, remember that 1 is where you start. Is this good or bad? Well, we will refrain from pointing fingers for now.
All arrays in BoxLang are passed by passed by reference. Please remember this when working with arrays and passing them to functions. There is also the passby=reference|value attribute to function arguments where you can decide whether to pass by reference or value.
Arrays in Code
Let's do some code samples:
fruits = [ "banana", "papaya", "kiwi", "apple", "orange", "grape" ]
// Basic array operations
println( "Length: " & fruits.len() )
println( "First fruit: " & fruits.first() )
println( "Last fruit: " & fruits.last() )
// Functional programming with arrays
println( "--- Functional Operations ---" )
// Map - transform each element with a lambda, no access to outside scopes
uppercaseFruits = fruits.map( fruit -> fruit.ucase() )
println( "Uppercase: " & uppercaseFruits.toString() )
// Filter - get elements matching a condition
longFruits = fruits.filter( (fruit) -> fruit.len() > 5 )
println( "Long names: " & longFruits.toString() )
// Reduce - combine all elements into a single value
totalLength = fruits.reduce( (sum, fruit) -> sum + fruit.len(), 0 )
println( "Total characters: " & totalLength )
// Find - get first matching element
foundFruit = fruits.find( (fruit) -> fruit.startsWith("a") )
println( "First fruit starting with 'a': " & foundFruit )
// Sort order elements
sortedFruits = fruits.sort( (a, b) -> a.compareNoCase(b) )
println( "Sorted: " & sortedFruits.toString() )
// Chain operations together
result = fruits
.filter( fruit -> fruit.len() <= 6 )
.map( fruit -> fruit.ucase() )
.sort()
println( "Chained operations: " & result.toString() )
// forEach - perform action on each element
println( "--- Individual Fruits ---" )
fruits.each( (fruit, index) => {
println( "#index#: #fruit#" )
} )
Please note that all member functions can also be used as traditional array functions. However, member functions look much better for readability.
Tip: You can use the toString() call on any array to get a string representation of its values: grid.toString()
Multi-Dimensional Arrays
While BoxLang arrays are inherently one-dimensional, you can create multi-dimensional structures by nesting arrays within arrays. This approach provides flexibility for representing matrices, tables, grids, and other complex data structures.
// Get dimensions of a 2D array
function getDimensions( array2D ) {
return {
"rows": array2D.len(),
"cols": array2D.len() > 0 ? array2D[1].len() : 0
}
}
// Transpose a 2D array (swap rows and columns)
function transpose( array2D ) {
if( array2D.len() == 0 ) return []
result = []
for( col = 1; col <= array2D[1].len(); col++ ) {
newRow = []
for( row = 1; row <= array2D.len(); row++ ) {
newRow.append( array2D[row][col] )
}
result.append( newRow )
}
return result
}
// Flatten a 2D array into 1D
function flatten( array2D ) {
return array2D.reduce( (flat, row) => {
flat.addAll( row )
return flat
}, [] )
}
Best Practices
Consistent Structure: Ensure all sub-arrays have the same length when representing regular grids
Bounds Checking: Always verify array indices exist before accessing nested elements
Memory Considerations: Large multi-dimensional arrays can consume significant memory
Initialization: Pre-populate arrays with default values to avoid null reference errors
Documentation: Clearly document the expected structure and dimensions of your nested arrays
Multi-dimensional arrays in BoxLang provide powerful data organization capabilities while maintaining the simplicity of single-dimensional array operations.
// Sort an array
meals.sort( "textnocase" );
// Clear the array
meals.clear();
// Go on a diet
meals.delete( "Dessert" );
meals.deleteAt( 4 );
// Iterate
meals.each( function( element, index) {
systemOutput( element & " " & index );
} );
// Filter an array
meals.filter( function( item ){
return item.findNoCase( "unch" ) gt 0 ? true : false;
} );
// Convert to a list
meals.toList();
// Map/ Reduce
complexData = [ {a: 4}, {a: 18}, {a: 51} ];
newArray = arrayMap( complexData, function(item){
return item.a;
});
writeDump(newArray);
complexData = [ {a: 4}, {a: 18}, {a: 51} ];
sum = arrayReduce( complexData, function(prev, element)
{
return prev + element.a;
}, 0 );
writeDump(sum);
Negative Indices
BoxLang also supports the concept of negative indices. This allows you to retrieve the elements from the end of the array backward. So you can easily count back instead of counting forwards:
BoxLang supports the slicing of an array via the arraySlice() method or the slice() member function, respectively. Slicing allows you to return a new array from the start position up to the count of elements you want.
You can use different constructs for looping over arrays:
for loops
loop constructs
each() closures
for( var thisMeal in meals ){
systemOutput( "I just had #thisMeal#" );
}
for( var x = 1; x lte meals.len(); x++ ){
systemOutput( "I just had #meals[ x ]#" );
}
meals.each( function( element, index ){
systemOutput( "I just had #element#" );
} );
bx:loop( from=1, to=meals.len(), index=x ){
systemOutput( "I just had #meals[ x ]#" );
}
Multi-Threaded Looping
BoxLang allows you to leverage the each() operations in a multi-threaded fashion. The arrayEach() or each() functions allow for a parallel and maxThreads arguments so the iteration can happen concurrently on as many maxThreads as supported by your JVM.
This is incredibly awesome, as your callback will now be called concurrently! However, please note that once you enter concurrency land, you should shiver and tremble. Thread concurrency will be of the utmost importance, and you must ensure that scoping is done correctly and that appropriate locking strategies are in place when accessing shared scopes and/or resources. Here is where unmodifiable arrays, structs, and queries can help.
Arrays also allow the usage of the spread operator syntax to quickly copy all or part of an existing array or object into another array or object. This operator is used by leveraging three dots ... in specific expressions.
The Spread syntax allows an iterable such as an array expression or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. Here are some examples to help you understand this operator:
Function Calls
numbers = [ 1, 2, 3 ]
function sum( x, y, z ){
return x + y + z;
}
// Call the function using the spread operator
results = sum( ...numbers ) // 6
// Ignore the others
numbers = [ 1, 2, 3, 4, 5 ]
results = sum( ...numbers ) // 6
The rest operator is similar to the spread operator but behaves oppositely. Instead of expanding the literals, it contracts them into an array you designate via the ...{name} syntax. You can use this to define endless arguments for a function, for example. In this case, I can create a dynamic findBy function that takes in multiple criteria name-value pairs.
All arrays and structures offer the ability to listen to changes to themselves. This is all done via our $bx metadata object available on all arrays/structures. You will call the registerChangeListener() function to register a closure/lambda that will listen to changes on the array. You can listen:
To all changes in the array
To a specific index in the array
However, you must EXPLICITLY return the value that will be stored in the change.
// Listen to all changes in the array
array.$bx.registerChangeListener( closure/lambda )
// Listen to a specific index in the array
array.$bx.registerChangeListener( index, closure/lambda )
Please note that this change listener will fire on every array access, so ensure it's performant.
The signature of the closure/lambda is the following
( Key key, any newValue, any oldValue, array ) => {}
Please note that the Key is a BoxLang Key object, which simulates a case-insensitive string. You can use methods on it like:
getName()
getNameNoCase()
toString()
Here are a few simple examples:
fruits = [ "banana", "papaya", "kiwi", "apple" ]
// Listen to all changes of the array: add, remove, replace, etc.
fruits.registerChangeListener( (key, newValue, oldValue, array )=>{
println( "New value: " & newValue );
println( "Old value: " & oldValue );
return newValue;
} )
fruits.append( "pineapple" )
fruits.deleteAt( 1 )
println( fruits )
Here is another example when listening to a specific index: