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

Sources/ucode/docs/tutorials/05-dictionaries.md

  1 Dictionaries in ucode (also referred to as objects) are key-value collections
  2 that provide efficient lookups by key. Unlike arrays which use numeric indices,
  3 dictionaries use string keys to access values. Understanding how dictionaries
  4 are implemented in ucode and their distinctive characteristics will help you
  5 write more efficient and effective code.
  6 
  7 ## Key Characteristics of Ucode Dictionaries
  8 
  9 ### Hash Table Implementation with Ordered Keys
 10 
 11 Ucode dictionaries are implemented as ordered hash tables, which means:
 12 - They offer fast O(1) average-case lookups by key
 13 - Keys are hashed to determine storage location
 14 - Memory allocation is dynamic and grows as needed
 15 - Unlike arrays, memory is not allocated contiguously
 16 - Key order is preserved based on declaration or assignment sequence
 17 - Keys can be reordered using `sort()`
 18 
 19 ### String-Only Keys with Important Limitations
 20 
 21 One important limitation of ucode dictionaries:
 22 - All keys must be strings
 23 - Non-string keys are implicitly converted to strings
 24 - Numeric keys become string representations (e.g., `5` becomes `"5"`)
 25 - This differs from JavaScript where objects can use Symbols as keys
 26 
 27 #### Warning: Null Byte Truncation in Keys
 28 
 29 A critical implementation detail to be aware of is that dictionary keys
 30 containing null bytes (`\0`) will be silently truncated at the first null byte:
 31 
 32 ```
 33 let dict = {"foo\0bar": 123};
 34 print(dict.foo);  // 123
 35 print(exists(dict, "foo\0bar"));  // true - lookups truncate the key as well
 36 print(exists(dict, "foo"));  // true
 37 ```
 38 
 39 This happens because the underlying hash table implementation treats keys as
 40 C-style null-terminated strings. Truncation applies to lookups as well, so the
 41 entry written under `"foo\0bar"` is stored - and found - under the truncated
 42 key `"foo"`; the two keys collide. While this behavior may change in future
 43 versions of ucode, you should currently:
 44 
 45 - Never use keys containing null bytes
 46 - Sanitize any untrusted external input used as dictionary keys
 47 - Be especially careful when using binary data or user input as keys
 48 
 49 This issue can lead to subtle bugs and potential security vulnerabilities if
 50 malicious users craft input with embedded null bytes to manipulate key lookups.
 51 
 52 ### Type Flexibility for Values
 53 
 54 Like arrays, dictionary values in ucode can be of any type:
 55 - Booleans, numbers (integers and doubles), strings
 56 - Objects and arrays (allowing nested structures)
 57 - Functions and null values
 58 - Different keys can store different value types
 59 
 60 ### Reference Semantics
 61 
 62 Dictionaries are reference types in ucode:
 63 - Assigning a dictionary to a new variable creates a reference, not a copy
 64 - Modifying a dictionary through any reference affects all references
 65 - Equality comparisons test reference identity, not structural equality
 66 
 67 ## Core Dictionary Functions
 68 
 69 ### Dictionary Information Functions
 70 
 71 #### {@link module:core#length|length(x)} → {number}
 72 
 73 Returns the number of keys in a dictionary.
 74 
 75 ```
 76 let user = {name: "Alice", age: 30, role: "Admin"};
 77 length(user); // 3
 78 
 79 let empty = {};
 80 length(empty); // 0
 81 ```
 82 
 83 For dictionaries, `length()` returns the count of keys. If the input is not an
 84 array, string, or object, `length()` returns null.
 85 
 86 #### {@link module:core#keys|keys(obj)} → {Array}
 87 
 88 Returns an array containing all keys in the dictionary.
 89 
 90 ```
 91 let config = {debug: true, timeout: 500, retries: 3};
 92 keys(config); // ["debug", "timeout", "retries"]
 93 ```
 94 
 95 Unlike many other languages, ucode maintains key ordering based on declaration
 96 or assignment order. Keys are returned in the same order they were defined or
 97 assigned.
 98 
 99 #### {@link module:core#values|values(obj)} → {Array}
100 
101 Returns an array containing all values in the dictionary.
102 
103 ```
104 let counts = {apples: 5, oranges: 10, bananas: 7};
105 values(counts); // [5, 10, 7]
106 ```
107 
108 The returned values correspond to the declaration/assignment order of keys in
109 the dictionary, matching the order that would be returned by `keys()`.
110 
111 #### {@link module:core#exists|exists(obj, key)} → {boolean}
112 
113 Checks whether a key exists in a dictionary.
114 
115 ```
116 let settings = {theme: "dark", fontSize: 16};
117 exists(settings, "theme"); // true
118 exists(settings, "language"); // false
119 ```
120 
121 This function offers a straightforward way to check for key existence without
122 accessing the value.
123 
124 #### Checking if a Value is a Dictionary
125 
126 To determine if a value is a dictionary (object), use the `type()` function:
127 
128 ```
129 function isObject(value) {
130     return type(value) == "object";
131 }
132 
133 isObject({key: "value"}); // true
134 isObject([1, 2, 3]); // false
135 isObject("string"); // false
136 isObject(null); // false
137 ```
138 
139 ### Manipulation Functions
140 
141 In ucode, dictionary manipulation is performed primarily through direct property
142 access using dot notation or bracket notation.
143 
144 #### Adding or Updating Properties
145 
146 ```
147 let user = {name: "Bob"};
148 
149 // Adding new properties
150 user.age = 25;
151 user["email"] = "bob@example.com";
152 
153 // Updating existing properties
154 user.name = "Robert";
155 user["age"] += 1;
156 
157 print(user); // { "name": "Robert", "age": 26, "email": "bob@example.com" }
158 ```
159 
160 #### Removing Properties
161 
162 Properties can be removed using the `delete` operator:
163 
164 ```
165 let product = {id: "p123", name: "Laptop", price: 999, discontinued: false};
166 
167 delete product.discontinued;
168 print(product); // { "id": "p123", "name": "Laptop", "price": 999 }
169 
170 delete product["price"];
171 print(product); // { "id": "p123", "name": "Laptop" }
172 ```
173 
174 #### Merging Dictionaries
175 
176 Ucode supports using spread expressions to merge dictionaries elegantly:
177 
178 ```
179 let defaults = {theme: "light", fontSize: 12, notifications: true};
180 let userSettings = {theme: "dark"};
181 
182 // Merge dictionaries with spread syntax
183 let merged = {...defaults, ...userSettings};
184 print(merged); // { "theme": "dark", "fontSize": 12, "notifications": true }
185 ```
186 
187 When merging with spread syntax, properties from later objects overwrite those
188 from earlier objects if the keys are the same. This provides a clean way to
189 implement default options with overrides:
190 
191 ```
192 // Apply user preferences with fallbacks
193 let config = {
194     ...systemDefaults,
195     ...globalSettings,
196     ...userPreferences
197 };
198 ```
199 
200 For situations requiring more complex merging logic, you can implement a custom
201 function:
202 
203 ```
204 function merge(target, ...sources) {
205     for (source in sources) {
206         for (key in keys(source)) {
207             target[key] = source[key];
208         }
209     }
210     return target;
211 }
212 
213 let defaults = {theme: "light", fontSize: 12, notifications: true};
214 let userSettings = {theme: "dark"};
215 let merged = merge({}, defaults, userSettings);
216 print(merged); // { "theme": "dark", "fontSize": 12, "notifications": true }
217 ```
218 
219 Note that this performs a shallow merge. For nested objects, a deep merge would
220 be needed:
221 
222 ```
223 function deepMerge(target, ...sources) {
224     for (source in sources) {
225         if (type(source) !== "object") continue;
226 
227         for (key in keys(source)) {
228             if (type(source[key]) == "object" && type(target[key]) == "object") {
229                 // Recursively merge nested objects
230                 target[key] = deepMerge({...target[key]}, source[key]);
231             } else {
232                 // For primitive values or when target key doesn't exist/isn't an object
233                 target[key] = source[key];
234             }
235         }
236     }
237 
238     return target;
239 }
240 
241 let userProfile = {
242     name: "Alice",
243     preferences: {
244         theme: "light",
245         sidebar: {
246             visible: true,
247             width: 250
248         }
249     }
250 };
251 
252 let updates = {
253     preferences: {
254         theme: "dark",
255         sidebar: {
256             width: 300
257         }
258     }
259 };
260 
261 let merged = deepMerge({}, userProfile, updates);
262 print(merged);
263 /* Result:
264 { "name": "Alice", "preferences": { "theme": "dark", "sidebar": { "visible": true, "width": 300 } } }
265 */
266 ```
267 
268 ### Iteration Techniques
269 
270 #### Iterating with for-in
271 
272 The most common way to iterate through a dictionary is using `for-in`:
273 
274 ```
275 let metrics = {visits: 1024, conversions: 85, bounceRate: 0.35};
276 
277 for (key in metrics) {
278     printf("%s: %J\n", key, metrics[key]);
279 }
280 // Output:
281 // visits: 1024
282 // conversions: 85
283 // bounceRate: 0.35
284 ```
285 
286 #### Iterating over Entries (Key-Value Pairs)
287 
288 A more advanced iteration technique gives access to both keys and values:
289 
290 ```
291 let product = {name: "Widget", price: 19.99, inStock: true};
292 
293 for (key in keys(product)) {
294     let value = product[key];
295     printf("%s: %J\n", key, value);
296 }
297 ```
298 
299 #### Enhanced for-in Loop
300 
301 Ucode provides an enhanced for-in loop that can destructure keys and values:
302 
303 ```
304 let inventory = {apples: 50, oranges: 25, bananas: 30};
305 
306 for (item, quantity in inventory) {
307     printf("We have %d %s in stock\n", quantity, item);
308 }
309 // Output:
310 // We have 50 apples in stock
311 // We have 25 oranges in stock
312 // We have 30 bananas in stock
313 ```
314 
315 This syntax offers a more elegant way to work with both keys and values
316 simultaneously.
317 
318 ## Key Ordering and Sorting
319 
320 One distinctive feature of ucode dictionaries is their predictable key ordering.
321 Unlike many other languages where hash-based dictionaries have arbitrary or
322 implementation-dependent key ordering, ucode maintains key order based on
323 declaration or assignment sequence.
324 
325 ### Predictable Iteration Order
326 
327 When iterating through a dictionary, keys are always processed in their
328 insertion order:
329 
330 ```
331 let scores = {};
332 scores.alice = 95;
333 scores.bob = 87;
334 scores.charlie = 92;
335 
336 // Keys will be iterated in the exact order they were added
337 for (name in scores) {
338     printf("%s: %d\n", name, scores[name]);
339 }
340 // Output will consistently be:
341 // alice: 95
342 // bob: 87
343 // charlie: 92
344 ```
345 
346 This predictable ordering applies to all dictionary operations: for-in loops,
347 `keys()`, and `values()`.
348 
349 ### Sorting Dictionary Keys
350 
351 You can explicitly reorder dictionary keys using the `sort()` function:
352 
353 ```
354 let stats = {
355     average: 72.5,
356     median: 68,
357     mode: 65,
358     range: 45
359 };
360 
361 // Sort keys alphabetically
362 sort(stats);
363 
364 // Now keys will be iterated in alphabetical order
365 for (metric in stats) {
366     printf("%s: %J\n", metric, stats[metric]);
367 }
368 // Output:
369 // average: 72.5
370 // median: 68
371 // mode: 65
372 // range: 45
373 ```
374 
375 Custom sorting is also supported:
376 
377 ```
378 let inventory = {
379     apples: 45,
380     bananas: 25,
381     oranges: 30,
382     grapes: 60
383 };
384 
385 // Sort by value (quantity) in descending order
386 sort(inventory, (k1, k2, v1, v2) => v2 - v1);
387 
388 // Keys will now be ordered by their associated values
389 for (fruit, quantity in inventory) {
390     printf("%s: %d\n", fruit, quantity);
391 }
392 // Output:
393 // grapes: 60
394 // apples: 45
395 // oranges: 30
396 // bananas: 25
397 ```
398 
399 This ability to maintain and manipulate key order makes ucode dictionaries
400 particularly useful for:
401 - Configuration objects where property order matters
402 - UI element definitions that should be processed in a specific sequence
403 - Data structures that need to maintain insertion chronology
404 
405 ## Advanced Dictionary Techniques
406 
407 ### Nested Dictionaries
408 
409 Dictionaries can contain other dictionaries, allowing for complex data
410 structures:
411 
412 ```
413 let company = {
414     name: "Acme Corp",
415     founded: 1985,
416     address: {
417         street: "123 Main St",
418         city: "Metropolis",
419         zipCode: "12345"
420     },
421     departments: {
422         engineering: {
423             headCount: 50,
424             projects: ["Alpha", "Beta", "Gamma"]
425         },
426         sales: {
427             headCount: 30,
428             regions: ["North", "South", "East", "West"]
429         }
430     }
431 };
432 
433 // Accessing nested properties
434 printf("Engineering headcount: %d\n", company.departments.engineering.headCount);
435 ```
436 
437 ### Dictionary as a Cache
438 
439 Dictionaries are excellent for implementing caches or memoization:
440 
441 ```
442 function memoizedFibonacci() {
443     let cache = {};
444 
445     // Return the actual fibonacci function with closure over cache
446     return function fib(n) {
447         // Check if result exists in cache
448         if (exists(cache, n)) {
449             return cache[n];
450         }
451 
452         // Calculate result for new inputs
453         let result;
454         if (n <= 1) {
455             result = n;
456         } else {
457             result = fib(n-1) + fib(n-2);
458         }
459 
460         // Store result in cache
461         cache[n] = result;
462         return result;
463     };
464 }
465 
466 let fibonacci = memoizedFibonacci();
467 printf("Fibonacci 40: %d\n", fibonacci(40)); // Fast computation due to caching
468 ```
469 
470 ### Using Dictionaries for Lookups
471 
472 Dictionaries excel at lookup tables and can replace complex conditional logic:
473 
474 ```
475 // Instead of:
476 function getStatusMessage(code) {
477     if (code == 200) return "OK";
478     else if (code == 404) return "Not Found";
479     else if (code == 500) return "Server Error";
480     // ...and so on
481     return "Unknown Status";
482 }
483 
484 // Use a dictionary:
485 let statusMessages = {
486     "200": "OK",
487     "404": "Not Found",
488     "500": "Server Error"
489 };
490 
491 function getStatusMessage(code) {
492     return statusMessages[code] ?? "Unknown Status";
493 }
494 ```
495 
496 ### Dictionary Patterns and Recipes
497 
498 #### Deep Clone
499 
500 Creating a deep copy of a dictionary with nested objects:
501 
502 ```
503 function deepCloneArray;   // forward declaration, see below
504 
505 function deepClone(obj) {
506     if (type(obj) != "object") {
507         return obj;
508     }
509 
510     let clone = {};
511     for (key in keys(obj)) {
512         if (type(obj[key]) == "object") {
513             clone[key] = deepClone(obj[key]);
514         } else if (type(obj[key]) == "array") {
515             clone[key] = deepCloneArray(obj[key]);
516         } else {
517             clone[key] = obj[key];
518         }
519     }
520     return clone;
521 }
522 
523 function deepCloneArray(arr) {
524     let result = [];
525     for (item in arr) {
526         if (type(item) == "object") {
527             push(result, deepClone(item));
528         } else if (type(item) == "array") {
529             push(result, deepCloneArray(item));
530         } else {
531             push(result, item);
532         }
533     }
534     return result;
535 }
536 ```
537 
538 Note the forward declaration of `deepCloneArray` at the top: ucode does not
539 hoist function declarations, so the body of `deepClone()` cannot call a
540 function that is declared later in the file. Without the forward declaration,
541 cloning a dictionary with an array value would fail with "Type error:
542 left-hand side is not a function".
543 
544 #### Dictionary Filtering
545 
546 Creating a new dictionary with only desired key-value pairs:
547 
548 ```
549 function filterObject(obj, filterFn) {
550     let result = {};
551     for (key in keys(obj)) {
552         if (filterFn(key, obj[key])) {
553             result[key] = obj[key];
554         }
555     }
556     return result;
557 }
558 
559 // Example: Keep only numeric values
560 let mixed = {a: 1, b: "string", c: 3, d: true, e: 4.5};
561 let numbersOnly = filterObject(mixed, (key, value) =>
562     type(value) == "int" || type(value) == "double"
563 );
564 print(numbersOnly); // { "a": 1, "c": 3, "e": 4.5 }
565 ```
566 
567 #### Object Mapping
568 
569 Transforming values in a dictionary while keeping the same keys:
570 
571 ```
572 function mapObject(obj, mapFn) {
573     let result = {};
574     for (key in keys(obj)) {
575         result[key] = mapFn(key, obj[key]);
576     }
577     return result;
578 }
579 
580 // Example: Double all numeric values
581 let prices = {apple: 1.25, banana: 0.75, cherry: 2.50};
582 let discountedPrices = mapObject(prices, (fruit, price) => price * 0.8);
583 print(discountedPrices); // { "apple": 1.0, "banana": 0.6, "cherry": 2.0 }
584 ```
585 
586 #### Dictionary Equality
587 
588 Comparing dictionaries by value instead of by reference:
589 
590 ```
591 function arrayEquals;   // forward declaration, see below
592 
593 function objectEquals(obj1, obj2) {
594     // Check if both are objects
595     if (type(obj1) != "object" || type(obj2) != "object") {
596         return obj1 === obj2;
597     }
598 
599     // Check key count
600     let keys1 = keys(obj1);
601     let keys2 = keys(obj2);
602     if (length(keys1) != length(keys2)) {
603         return false;
604     }
605 
606     // Check each key-value pair
607     for (key in keys1) {
608         if (!exists(obj2, key)) {
609             return false;
610         }
611 
612         if (type(obj1[key]) == "object" && type(obj2[key]) == "object") {
613             // Recursively check nested objects
614             if (!objectEquals(obj1[key], obj2[key])) {
615                 return false;
616             }
617         } else if (type(obj1[key]) == "array" && type(obj2[key]) == "array") {
618             // For arrays, we would need array equality check
619             if (!arrayEquals(obj1[key], obj2[key])) {
620                 return false;
621             }
622         } else if (obj1[key] !== obj2[key]) {
623             return false;
624         }
625     }
626     return true;
627 }
628 
629 function arrayEquals(arr1, arr2) {
630     if (length(arr1) != length(arr2)) {
631         return false;
632     }
633 
634     for (let i = 0; i < length(arr1); i++) {
635         if (type(arr1[i]) == "object" && type(arr2[i]) == "object") {
636             if (!objectEquals(arr1[i], arr2[i])) {
637                 return false;
638             }
639         } else if (type(arr1[i]) == "array" && type(arr2[i]) == "array") {
640             if (!arrayEquals(arr1[i], arr2[i])) {
641                 return false;
642             }
643         } else if (arr1[i] !== arr2[i]) {
644             return false;
645         }
646     }
647     return true;
648 }
649 ```
650 
651 Note the forward declaration of `arrayEquals` at the top: ucode does not hoist
652 function declarations, so the body of `objectEquals()` cannot call a function
653 that is declared later in the file. Without the forward declaration, comparing
654 dictionaries with array values would fail with "Type error: left-hand side is
655 not a function".
656 
657 ## Performance Considerations and Best Practices
658 
659 ### Hash Collision Impacts
660 
661 Since ucode dictionaries use hash tables:
662 - Hash collisions can occur (different keys hash to same value)
663 - Hash collision resolution affects performance
664 - As dictionaries grow large, performance degradation may occur
665 - Performance is generally consistent but can have occasional spikes due to rehashing
666 
667 ### Key Naming Considerations
668 
669 String keys have important implications:
670 - Choose short, descriptive keys to minimize memory usage
671 - Be consistent with key naming conventions
672 - Remember that property access via dot notation (`obj.prop`) and bracket notation (`obj["prop"]`) are equivalent
673 - Keys containing special characters or reserved words must use bracket notation: `obj["special-key"]`
674 
675 ### Memory Usage Optimization
676 
677 To optimize dictionary memory usage:
678 - Delete unused keys to prevent memory leaks
679 - Use shallow structures when possible
680 - Consider serialization for large dictionaries not actively used
681 - Be aware that circular references delay garbage collection until mark-sweep GC runs
682 
683 ```
684 // Circular reference example
685 let obj1 = {};
686 let obj2 = {ref: obj1};
687 obj1.ref = obj2; // Creates a circular reference
688 
689 // While reference counting won't collect these immediately,
690 // a mark-sweep GC run will eventually reclaim this memory
691 // when the objects become unreachable from the root scope
692 ```
693 
694 ### Performance Patterns
695 
696 #### Property Access Optimization
697 
698 When repeatedly accessing the same property in loops, consider caching:
699 
700 ```
701 // Less efficient - repeated property access
702 for (let i = 0; i < 1000; i++) {
703     processValue(config.complexComputedValue);
704 }
705 
706 // More efficient - cache the property
707 let cachedValue = config.complexComputedValue;
708 for (let i = 0; i < 1000; i++) {
709     processValue(cachedValue);
710 }
711 ```
712 
713 #### Key Existence Check Performance
714 
715 Different methods for checking key existence have varying performance
716 implications:
717 
718 ```
719 // Option 1: Using exists() - most explicit and readable
720 if (exists(user, "email")) {
721     sendEmail(user.email);
722 }
723 
724 // Option 2: Direct property access with null check
725 if (user.email != null) {
726     sendEmail(user.email);
727 }
728 
729 // Option 3: Using in operator with keys
730 if ("email" in keys(user)) {
731     sendEmail(user.email);
732 }
733 ```
734 
735 Option 1 is typically the most performant as it's specifically designed for
736 this purpose.
737 
738 ### Dictionary Implementation Details
739 
740 Understanding internal implementation details can help write more efficient code:
741 
742 1. **Initial Capacity**: Dictionaries start with a small capacity and grow as needed
743 2. **Load Factor**: When dictionaries reach a certain fullness threshold, they're resized
744 3. **Hash Function**: Keys are hashed using a specialized string hashing function
745 4. **Collision Resolution**: Ucode typically uses open addressing with linear probing
746 5. **Deletion**: When keys are deleted, they're marked as deleted but space isn't reclaimed until rehashing
747 6. **Order Preservation**: Unlike many hash table implementations, ucode tracks and maintains insertion order
748 
749 These implementation details explain why:
750 - Iterating over a dictionary with many deleted keys might be slower
751 - Adding many keys may trigger occasional performance pauses for rehashing
752 - Key order is consistent and predictable, matching declaration/assignment order
753 - Dictionaries can be deliberately reordered using `sort()`
754 
755 ## Conclusion
756 
757 Dictionaries in ucode provide a powerful and flexible way to organize data by
758 key-value relationships. By understanding their implementation characteristics
759 and following best practices, you can effectively leverage dictionaries for
760 everything from simple configuration storage to complex nested data structures.
761 
762 Remember that dictionaries excel at:
763 - Fast lookups by string key
764 - Dynamic property addition and removal
765 - Representing structured data
766 - Implementing caches and lookup tables
767 
768 When working with large dictionaries or performance-critical code, consider the
769 memory usage patterns and optimization techniques described in this article to
770 ensure your code remains efficient and maintainable.

This page was automatically generated by LXR 0.3.1.  •  OpenWrt