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

This page was automatically generated by LXR 0.3.1.  •  OpenWrt