• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/docs/tutorials/04-arrays.md

  1 Arrays in ucode are ordered collections that can store any ucode value. Unlike
  2 many other scripting languages where arrays are implemented as hash tables or
  3 linked lists, ucode arrays are true arrays in memory. This implementation detail
  4 provides several distinctive characteristics that developers should understand
  5 when working with arrays in ucode.
  6 
  7 ## Key Characteristics of Ucode Arrays
  8 
  9 ### True Memory Arrays
 10 
 11 Ucode arrays are implemented as true arrays in memory, which means:
 12 - They offer fast random access to elements by index
 13 - They are stored contiguously in memory
 14 - Memory allocation expands as needed to accommodate the highest used index
 15 
 16 ### Sparse Array Behavior
 17 
 18 Because ucode arrays are true arrays in memory:
 19 - Memory is always allocated contiguously up to the highest used index
 20 - All positions (including unused ones) consume memory
 21 - "Empty" or unused positions contain `null` values
 22 - There is no special optimization for sparse arrays
 23 
 24 ### Negative Index Support
 25 
 26 Ucode arrays provide convenient negative indexing:
 27 - `-1` refers to the last element
 28 - `-2` refers to the second-last element
 29 - And so on, allowing easy access to elements from the end of the array
 30 
 31 ### Type Flexibility
 32 
 33 Arrays in ucode can hold any ucode value type:
 34 - Booleans, numbers (integers and doubles), strings
 35 - Objects and arrays (allowing nested arrays)
 36 - Functions and null values
 37 - No type restrictions between elements (unlike typed arrays in some languages)
 38 
 39 ## Core Array Functions
 40 
 41 ### Array Information Functions
 42 
 43 #### {@link module:core#length|length(x)} → {number}
 44 
 45 Returns the number of elements in an array. This is one of the most fundamental
 46 array operations in ucode.
 47 
 48 ```
 49 let fruits = ["apple", "banana", "orange"];
 50 length(fruits); // 3
 51 
 52 let sparse = [];
 53 sparse[10] = "value";
 54 length(sparse); // 11 (includes empty slots)
 55 ```
 56 
 57 For arrays, `length()` returns the highest index plus one, which means it
 58 includes empty slots in sparse arrays. If the input is not an array, string, or
 59 object, `length()` returns null.
 60 
 61 #### {@link module:core#index|index(arr_or_str, needle)} → {number}
 62 
 63 Searches for a value in an array and returns the index of the first matching occurrence.
 64 
 65 ```
 66 let colors = ["red", "green", "blue", "green"];
 67 index(colors, "green"); // 1 (returns first match)
 68 index(colors, "yellow"); // -1 (not found)
 69 ```
 70 
 71 Unlike many other languages where array search functions return -1 or null for
 72 non-matching items, `index()` in ucode specifically returns -1 when the value
 73 isn't found. It returns null only if the first argument is neither an array nor
 74 a string.
 75 
 76 #### {@link module:core#rindex|rindex(arr_or_str, needle)} → {number}
 77 
 78 Similar to `index()`, but searches backward from the end of the array:
 79 
 80 ```
 81 let colors = ["red", "green", "blue", "green"];
 82 rindex(colors, "green"); // 3 (last occurrence)
 83 ```
 84 
 85 #### Checking if a Value is an Array
 86 
 87 To determine if a value is an array, use the `type()` function:
 88 
 89 ```
 90 function isArray(value) {
 91     return type(value) == "array";
 92 }
 93 
 94 isArray([1, 2, 3]); // true
 95 isArray("string"); // false
 96 isArray({key: "value"}); // false
 97 isArray(null); // false
 98 ```
 99 
100 The `type()` function is extremely useful for defensive programming, especially
101 in ucode where functions often need to determine the type of their arguments to
102 process them correctly.
103 
104 ### Manipulation Functions
105 
106 #### {@link module:core#push|push(arr, ...values)} → {*}
107 
108 Adds one or more elements to the end of an array and returns the last pushed
109 value.
110 
111 ```
112 let x = [1, 2, 3];
113 push(x, 4, 5, 6); // 6
114 print(x); // [1, 2, 3, 4, 5, 6]
115 ```
116 
117 Returns null if the array was empty or if a non-array argument was passed.
118 
119 #### {@link module:core#pop|pop(arr)} → {*}
120 
121 Removes the last element from an array and returns it.
122 
123 ```
124 let x = [1, 2, 3];
125 let lastItem = pop(x); // 3
126 print(x); // [1, 2]
127 ```
128 
129 Returns null if the array was empty or if a non-array argument was passed.
130 
131 #### {@link module:core#unshift|unshift(arr, ...values)} → {*}
132 
133 Adds one or more elements to the beginning of an array and returns the last
134 value added.
135 
136 ```
137 let x = [3, 4, 5];
138 unshift(x, 1, 2); // 2
139 print(x); // [1, 2, 3, 4, 5]
140 ```
141 
142 #### {@link module:core#shift|shift(arr)} → {*}
143 
144 Removes the first element from an array and returns it.
145 
146 ```
147 let x = [1, 2, 3];
148 let firstItem = shift(x); // 1
149 print(x); // [2, 3]
150 ```
151 
152 Returns null if the array was empty or if a non-array argument was passed.
153 
154 ### Transformation Functions
155 
156 #### {@link module:core#map|map(arr, fn)} → {Array}
157 
158 Creates a new array populated with the results of calling a provided function on
159 every element in the calling array.
160 
161 ```
162 let numbers = [1, 2, 3, 4];
163 let squares = map(numbers, x => x * x); // [1, 4, 9, 16]
164 ```
165 
166 Note: The callback function receives three arguments:
167 1. The current element value
168 2. The current index
169 3. The array being processed
170 
171 ```
172 let values = map(["foo", "bar", "baz"], function(value, index, array) {
173   return `${index}: ${value} (from array of length ${length(array)})`;
174 });
175 ```
176 
177 ##### Important Pitfall with Built-in Functions
178 
179 A common mistake when using `map()` is passing a built-in function directly as
180 the callback. Consider this example attempting to convert an array of strings to
181 integers:
182 
183 ```
184 // ⚠️ INCORRECT: This will not work as expected!
185 let strings = ["10", "32", "13"];
186 let nums = map(strings, int);  // Results will be unpredictable!
187 ```
188 
189 This fails because the `map()` function calls the callback with three arguments:
190 1. The current value (`"10"`, `"32"`, etc.)
191 2. The current index (`0`, `1`, `2`)
192 3. The original array (`["10", "32", "13"]`)
193 
194 So what actually happens is equivalent to:
195 ```
196 int("10", 0, ["10", "32", "13"])  // Interprets 0 as base parameter!
197 int("32", 1, ["10", "32", "13"])  // Interprets 1 as base parameter!
198 int("13", 2, ["10", "32", "13"])  // Interprets 2 as base parameter!
199 ```
200 
201 The second argument to `int()` is interpreted as the numeric base, causing
202 unexpected conversion results:
203 
204 - `"10"` in base 0 is interpreted as decimal 10 (base 0 is a special case that auto-detects the base)
205 - `"32"` in base 1 produces `NaN` because base 1 is invalid (a numeral system needs at least 2 distinct digits)
206 - `"13"` in base 2 produces `1` because in binary only `0` and `1` are valid digits - it converts `"1"` successfully and stops at the invalid character `"3"`
207 
208 The actual result would be `[10, NaN, 1]`, which is certainly not what you'd
209 expect when trying to convert string numbers to integers!
210 
211 To fix this, wrap the function call in an arrow function or a regular function
212 that controls the number of arguments:
213 
214 ```
215 // ✓ CORRECT: Using arrow function to control arguments
216 let strings = ["10", "32", "13"];
217 let nums = map(strings, x => int(x));  // [10, 32, 13]
218 
219 // Alternative approach using a named function
220 function toInt(str) {
221   return int(str);
222 }
223 let nums2 = map(strings, toInt);  // [10, 32, 13]
224 ```
225 
226 This pattern applies to many other built-in functions like `length()`, `trim()`,
227 `b64enc()`, etc. Always wrap built-in functions when using them with `map()` to
228 ensure they receive only the intended arguments.
229 
230 #### {@link module:core#filter|filter(arr, fn)} → {Array}
231 
232 Creates a new array with all elements that pass the test implemented by the
233 provided function.
234 
235 ```
236 let numbers = [1, 2, 3, 4, 5, 6];
237 let evens = filter(numbers, x => x % 2 == 0); // [2, 4, 6]
238 ```
239 
240 The callback function receives the same three arguments as in `map()`.
241 
242 #### {@link module:core#sort|sort(arr, fn)} → {Array}
243 
244 Sorts the elements of an array in place and returns the sorted array, optionally
245 using a custom compare function.
246 
247 ```
248 let numbers = [3, 1, 4, 2];
249 sort(numbers); // [1, 2, 3, 4]
250 ```
251 
252 With a custom compare function:
253 
254 ```
255 let people = [
256   { name: "Alice", age: 25 },
257   { name: "Bob", age: 30 },
258   { name: "Charlie", age: 20 }
259 ];
260 
261 sort(people, (a, b) => a.age - b.age);
262 // [{ name: "Charlie", age: 20 }, { name: "Alice", age: 25 }, { name: "Bob", age: 30 }]
263 ```
264 
265 #### {@link module:core#reverse|reverse(arr)} → {Array}
266 
267 Returns a new array with the order of all elements reversed.
268 
269 ```
270 let arr = [1, 2, 3];
271 reverse(arr); // [3, 2, 1]
272 ```
273 
274 This function also works on strings:
275 
276 ```
277 reverse("hello"); // "olleh"
278 ```
279 
280 Returns null if the argument is neither an array nor a string.
281 
282 #### {@link module:core#uniq|uniq(array)} → {Array}
283 
284 Creates a new array with all duplicate elements removed.
285 
286 ```
287 let array = [1, 2, 2, 3, 1, 4, 5, 4];
288 uniq(array); // [1, 2, 3, 4, 5]
289 ```
290 
291 Returns null if a non-array argument is given.
292 
293 ### Helper Functions and Recipes
294 
295 The ucode standard library provides essential array functions, but many common
296 operations must be implemented manually. Below, you'll find example
297 implementations for frequently needed array operations that aren't built into
298 the core library.
299 
300 These recipes demonstrate how to leverage ucode's existing functions to build
301 more complex array utilities.
302 
303 #### Array Intersection
304 
305 Returns a new array containing elements present in all provided arrays.
306 
307 ```
308 function intersect(...arrays) {
309   if (!length(arrays))
310     return [];
311 
312   let result = arrays[0];
313 
314   for (let i = 1; i < length(arrays); i++) {
315     result = filter(result, item => item in arrays[i]);
316   }
317 
318   return uniq(result);
319 }
320 
321 // Example usage:
322 let a = [1, 2, 3, 4];
323 let b = [2, 3, 5];
324 let c = [2, 3, 6];
325 intersect(a, b, c); // [2, 3]
326 ```
327 
328 This implementation takes advantage of ucode's `in` operator, which checks if a
329 value exists in an array using strict equality comparison. This makes the code
330 more concise than using `index()` and checking if the result is not -1.
331 
332 #### Array Merge/Concatenation
333 
334 Combines multiple arrays into a new array. Taking advantage of ucode's variadic
335 `push()` function with the spread operator provides an elegant solution:
336 
337 ```
338 function merge(...arrays) {
339   let result = [];
340 
341   for (arr in arrays) {
342     push(result, ...arr);  // Spreads all elements from the array directly into push
343   }
344 
345   return result;
346 }
347 
348 // Example usage:
349 let a = [1, 2];
350 let b = [3, 4];
351 let c = [5, 6];
352 merge(a, b, c); // [1, 2, 3, 4, 5, 6]
353 ```
354 
355 This implementation leverages the variadic nature of `push()`, which accepts any
356 number of arguments. The spread operator (`...`) unpacks each array, passing all
357 its elements as individual arguments to `push()`. This is both more efficient
358 and more readable than nested loops.
359 
360 For processing very large arrays, you might want to use a batching approach to
361 avoid potential call stack limitations:
362 
363 ```
364 function mergeWithBatching(...arrays) {
365   let result = [];
366   const BATCH_SIZE = 1000;
367 
368   for (arr in arrays) {
369     // Handle array in batches to avoid excessive function arguments
370     for (let i = 0; i < length(arr); i += BATCH_SIZE) {
371       let batch = slice(arr, i, i + BATCH_SIZE);
372       push(result, ...batch);
373     }
374   }
375 
376   return result;
377 }
378 ```
379 
380 #### Array Difference
381 
382 Returns elements in the first array not present in subsequent arrays.
383 
384 ```
385 function difference(array, ...others) {
386   return filter(array, item => {
387     for (other in others) {
388       if (item in other)
389         return false;
390     }
391     return true;
392   });
393 }
394 
395 // Example usage:
396 let a = [1, 2, 3, 4, 5];
397 let b = [2, 3];
398 let c = [4];
399 difference(a, b, c); // [1, 5]
400 ```
401 
402 This implementation uses the `in` operator for concise and efficient membership
403 testing, filtering out any elements from the first array that appear in any of
404 the subsequent arrays.
405 
406 #### Array Chunk
407 
408 Splits an array into chunks of specified size.
409 
410 ```
411 function chunk(array, size) {
412   if (size <= 0)
413     return [];
414 
415   let result = [];
416 
417   for (let i = 0; i < length(array); i += size) {
418     push(result, slice(array, i, i + size));
419   }
420 
421   return result;
422 }
423 
424 // Example usage:
425 let nums = [1, 2, 3, 4, 5, 6, 7, 8];
426 chunk(nums, 3); // [[1, 2, 3], [4, 5, 6], [7, 8]]
427 ```
428 
429 This implementation uses a counting `for` loop combined with `slice()`, which is
430 both more idiomatic and more efficient. The approach:
431 
432 1. Iterates through the array in steps of `size`
433 2. Uses `slice()` to extract chunks of the appropriate size
434 3. Automatically handles the last chunk being smaller if the array length isn't divisible by the chunk size
435 
436 This pattern leverages ucode's built-in functions for cleaner, more maintainable
437 code. No temporary variables are needed to track the current chunk or count,
438 making the implementation more straightforward.
439 
440 #### Array Sum
441 
442 Calculates the sum of all numeric elements in an array.
443 
444 ```
445 function sum(array) {
446   let result = 0;
447 
448   for (item in array) {
449     if (type(item) == "int" || type(item) == "double")
450       result += item;
451   }
452 
453   return result;
454 }
455 
456 // Example usage:
457 let nums = [1, 2, 3, 4, 5];
458 sum(nums); // 15
459 ```
460 
461 #### Array Flatten
462 
463 Flattens a nested array structure.
464 
465 ```
466 function flatten(array, depth) {
467   if (depth === undefined)
468     depth = 1;
469 
470   let result = [];
471 
472   for (item in array) {
473     if (type(item) == "array" && depth > 0) {
474       let flattened = flatten(item, depth - 1);
475       for (subItem in flattened) {
476         push(result, subItem);
477       }
478     } else {
479       push(result, item);
480     }
481   }
482 
483   return result;
484 }
485 
486 // Example usage:
487 let nested = [1, [2, [3, 4], 5], 6];
488 flatten(nested);     // [1, 2, [3, 4], 5, 6]
489 flatten(nested, 2);  // [1, 2, 3, 4, 5, 6]
490 ```
491 
492 ## Advanced Array Techniques and Considerations
493 
494 ### Memory Management
495 
496 When working with arrays in ucode, you should understand several important
497 memory characteristics that affect performance and resource usage:
498 
499 #### Sparse Array Memory Implications
500 
501 Since ucode arrays are true arrays in memory, array memory consumption scales
502 linearly with the highest index used, regardless of how many elements are
503 actually stored:
504 
505 ```
506 let arr = [];
507 arr[1000000] = "value"; // Allocates memory for 1,000,001 pointers
508 ```
509 
510 Important technical details about ucode array memory usage:
511 - Each array element consumes pointer-sized memory (4 bytes on 32-bit systems, 8 bytes on 64-bit systems)
512 - No optimizations exist for sparse arrays - every position up to the highest index is allocated
513 - When an array grows beyond its capacity, it's reallocated with a growth factor of 1.5
514 - Memory is allocated even for "empty" slots (which contain the `null` value)
515 
516 For example, on a 64-bit system, creating an array with a single element at
517 index 1,000,000 would consume approximately 8MB of memory (1,000,001 * 8 bytes),
518 even though only one actual value is stored.
519 
520 ```
521 // Demonstrates memory consumption
522 let smallArray = [];
523 for (let i = 0; i < 10; i++)
524   smallArray[i] = i;
525 
526 let sparseArray = [];
527 sparseArray[1000000] = "far away";
528 
529 print(`Small array has ${length(smallArray)} elements\n`);
530 print(`Sparse array has ${length(sparseArray)} elements\n`);
531 // Even though only one value is actually set, memory is allocated for all positions
532 ```
533 
534 This behavior makes ucode arrays efficient for random access but potentially
535 wasteful for very sparse data structures. For data with large gaps or when
536 working on memory-constrained systems, consider alternative approaches like
537 objects with numeric string keys.
538 
539 #### Negative Index Implementation
540 
541 While negative indices provide convenient access to elements from the end of an
542 array, they involve an internal conversion process:
543 
544 ```
545 let arr = [1, 2, 3, 4, 5];
546 arr[-1]; // Internally converted to arr[length(arr) - 1], or arr[4]
547 arr[-3]; // Internally converted to arr[length(arr) - 3], or arr[2]
548 ```
549 
550 This conversion adds a small computational overhead compared to direct positive
551 indexing. For performance-critical code processing large arrays, consider using
552 positive indices when possible.
553 
554 #### Mixed-Type Arrays and Sorting
555 
556 Arrays in ucode can contain mixed types, offering great flexibility but
557 requiring careful handling, especially with operations like `sort()`:
558 
559 ```
560 let mixed = ["apple", 10, true, {name: "object"}, [1, 2]];
561 
562 // Sort behaves differently with mixed types
563 // Numbers come first, then arrays, then strings, then booleans, then objects
564 sort(mixed); // [10, [1, 2], "apple", true, {name: "object"}]
565 ```
566 
567 When sorting mixed-type arrays, consider implementing a custom comparison
568 function to define the sorting behavior explicitly:
569 
570 ```
571 function mixedTypeSort(a, b) {
572     // Sort by type first, then by value
573     let typeA = type(a);
574     let typeB = type(b);
575 
576     if (typeA != typeB) {
577         // Define a type precedence order
578         let typePrecedence = {
579             "int": 1,
580             "double": 2,
581             "string": 3,
582             "bool": 4,
583             "array": 5,
584             "object": 6
585         };
586         return typePrecedence[typeA] - typePrecedence[typeB];
587     }
588 
589     // If same type, compare values appropriately
590     if (typeA == "string" || typeA == "array")
591         return length(a) - length(b);
592     return a - b;
593 }
594 
595 // Now sorting is more predictable
596 sort(mixed, mixedTypeSort);
597 ```
598 
599 ### Performance Optimization
600 
601 When working with large arrays, consider these optimization techniques:
602 
603 1. **Pre-allocation**: Where possible, create arrays with known capacity rather than growing incrementally
604 2. **Batch operations**: Minimize individual push/pop/shift/unshift calls by processing in batches
605 3. **Avoid unnecessary copies**: Use in-place operations when possible
606 4. **Filter early**: Filter arrays early in processing pipelines to reduce subsequent operation sizes
607 
608 ### Array Deep Copying
609 
610 Since arrays are reference types, creating true copies requires special handling:
611 
612 ```
613 function deepCopy(arr) {
614     if (type(arr) != "array")
615         return arr;
616 
617     let result = [];
618     for (item in arr) {
619         if (type(item) == "array")
620             push(result, deepCopy(item));
621         else if (type(item) == "object")
622             push(result, deepCopyObject(item));
623         else
624             push(result, item);
625     }
626     return result;
627 }
628 
629 function deepCopyObject(obj) {
630     if (type(obj) != "object")
631         return obj;
632 
633     let result = {};
634     for (key in keys(obj)) {
635         if (type(obj[key]) == "array")
636             result[key] = deepCopy(obj[key]);
637         else if (type(obj[key]) == "object")
638             result[key] = deepCopyObject(obj[key]);
639         else
640             result[key] = obj[key];
641     }
642     return result;
643 }
644 ```
645 
646 This approach ensures all nested arrays and objects are properly copied rather
647 than referenced.

This page was automatically generated by LXR 0.3.1.  •  OpenWrt