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