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