1 /** 2 * Data structure for optimized "sparse" N-dimensional matrices 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.mapset; 15 16 static if (__VERSION__ >= 2083): 17 18 import std.algorithm.iteration; 19 import std.array; 20 import std.exception; 21 import std.typecons : tuple; 22 23 import ae.utils.aa; 24 import ae.utils.array : amap; 25 26 /** 27 Data structure for holding optimized "sparse" N-dimensional matrices. 28 The number of dimensions is arbitrary and can be varied at runtime. 29 30 Unlike classical sparse arrays/matrices, this data structure is 31 optimized not just for arrays which are mostly zero (or any given 32 value), but for any matrices where data is likely to repeat across 33 dimensions. 34 35 This is done by storing the data as a tree, with each depth 36 representing one dimension. A sub-tree is, thus, a sub-matrix 37 (slice of the matrix represented by the top-level tree) along the 38 top-level dimension. Each sub-tree is individually immutable, and 39 can therefore be shared within the same tree or even by several 40 instances of this type. 41 42 Dimension order need not be the same for all sub-trees. Even the 43 number of dimensions in siblings' sub-trees need not be the same; 44 i.e, the data structure is itself sparse with regards to the 45 dimensions. 46 47 As there is no explicit "value" type (associated with every set of 48 coordinates), this data structure can be seen as a representation 49 of a set of points, but it can be simulated by adding a "value" 50 dimension (in the same way how a gray-scale image can be 51 represented as a set of 3D points like a height-map). 52 53 An alternative way to look at this data structure is as a set of 54 maps (i.e. associative arrays). Thus, each layer of the tree stores 55 one possible map key, and under it, all the maps that have this key 56 with a specific value. Note that for this use case, DimValue needs 57 to have a value (configured using nullValue) which indicates the 58 absence of a certain key (dimension) in a map. 59 60 Params: 61 DimName = The type used to indicate the name (or index) of a dimension. 62 DimValue = The type for indicating a point on a dimension's axis. 63 nullValue = A value for DimValue indicating that a dimension is unset 64 */ 65 struct MapSet(DimName, DimValue, DimValue nullValue = DimValue.init) 66 { 67 struct Pair 68 { 69 DimValue value; 70 MapSet set; 71 } 72 73 struct Node 74 { 75 DimName dim; 76 Pair[] children; 77 78 immutable this(DimName dim, immutable Pair[] children) 79 { 80 // Zero children doesn't make sense. 81 // It would be the equivalent of an empty set, 82 // but then we should just use MapSet.emptySet instead. 83 assert(!children.empty, "Node with zero children"); 84 85 this.dim = dim; 86 this.children = children; 87 88 // Because each submatrix is immutable, we can 89 // pre-calculate the hash during construction. 90 hash = hashOf(dim) ^ hashOf(children); 91 92 size_t totalMembers = 0; 93 foreach (ref pair; children) 94 { 95 pair.set.assertDeduplicated(); 96 // Same as "Node with zero children" 97 assert(pair.set !is emptySet, "Empty set as submatrix"); 98 99 totalMembers += pair.set.count; 100 } 101 this.totalMembers = totalMembers; 102 } 103 104 immutable this(DimName dim, immutable MapSet[DimValue] children) 105 { 106 this(dim, children.byKeyValue.map!(kv => Pair(kv.key, kv.value)).array); 107 } 108 109 void toString(scope void delegate(const(char)[]) sink) const 110 { 111 import std.format : formattedWrite; 112 sink.formattedWrite("{ %(%s%), [ ", (&dim)[0..1]); 113 bool first = true; 114 foreach (ref pair; children) 115 { 116 if (first) 117 first = false; 118 else 119 sink(", "); 120 sink.formattedWrite("%s : %s", pair.value, pair.set); 121 } 122 sink(" ] }"); 123 } 124 125 private hash_t hash; 126 size_t totalMembers; 127 128 hash_t toHash() const 129 { 130 return hash; 131 } 132 133 bool opEquals(ref const typeof(this) s) const 134 { 135 return hash == s.hash && dim == s.dim && children == s.children; 136 } 137 } 138 139 /// Indicates the empty set. 140 /// May only occur at the top level (never as a submatrix). 141 private enum emptySetRoot = cast(immutable(Node)*)1; 142 143 /// If emptySetRoot, zero values. 144 /// If null, one value with zero dimensions. 145 /// Otherwise, pointer to node describing the next dimension. 146 immutable(Node)* root = emptySetRoot; 147 148 /// A set containing a single nil-dimensional element. 149 /// Holds exactly one value (a point with all dimensions at 150 /// nullValue). 151 static immutable unitSet = MapSet(null); 152 153 /// The empty set. Represents a set which holds zero values. 154 static immutable emptySet = MapSet(emptySetRoot); 155 156 /// Return the total number of items in this set. 157 size_t count() const 158 { 159 if (this is emptySet) 160 return 0; 161 if (this is unitSet) 162 return 1; 163 return root.totalMembers; 164 } 165 166 private struct SetSetOp { MapSet a, b; } 167 private struct SetDimOp { MapSet set; DimName dim; } 168 private struct Cache 169 { 170 /// For deduplication - key is value 171 MapSet[MapSet] instances; 172 /// Operations - things that operate recursively on subtrees 173 /// should be memoized here 174 MapSet[SetSetOp] merge, subtract, cartesianProduct; 175 MapSet[SetDimOp] remove, bringToFront; 176 MapSet[MapSet] optimize; 177 size_t[MapSet] uniqueNodes; 178 } 179 private static Cache cache; 180 181 /// Clear the global operations cache. 182 /// Because subtrees can be reused within the tree, a way of 183 /// memoizing operations across the entire tree (instead of just 184 /// across children of a single node, or siblings) is crucial for 185 /// performance. 186 /// Call this function to clear this cache. 187 static void clearCache() 188 { 189 cache = Cache.init; 190 } 191 192 /// Because MapSet operations benefit greatly from memoization, 193 /// maintaining a set of interned sets benefits performance. After 194 /// calling `clearCache`, call this method to re-register extant 195 /// live instances to the instance cache. 196 void addToCache() const 197 { 198 cache.instances.updateVoid( 199 this, 200 { 201 if (this !is emptySet && this !is unitSet) 202 foreach (ref pair; root.children) 203 pair.set.addToCache(); 204 return this; 205 }, 206 (ref MapSet set) 207 { 208 assert(set is this); 209 } 210 ); 211 } 212 213 /// Intern and deduplicate this MapSet. 214 /// Needs to be called only after constructing a MapSet manually 215 /// (by allocating, populating, and setting `root`). 216 MapSet deduplicate() const 217 { 218 MapSet deduplicated; 219 cache.instances.updateVoid( 220 this, 221 { 222 debug if (this !is emptySet && this !is unitSet) 223 foreach (ref pair; root.children) 224 pair.set.assertDeduplicated(); 225 deduplicated = this; 226 return this; 227 }, 228 (ref MapSet set) 229 { 230 deduplicated = set; 231 } 232 ); 233 return deduplicated; 234 } 235 236 private void assertDeduplicated() const { debug assert(this is emptySet || this is unitSet || cache.instances[this] is this); } 237 238 /// Count and return the total number of unique nodes in this MapSet. 239 size_t uniqueNodes() const 240 { 241 return cache.uniqueNodes.require(this, { 242 HashSet!MapSet seen; 243 void visit(MapSet set) 244 { 245 if (set is emptySet || set is unitSet || set in seen) 246 return; 247 seen.add(set); 248 foreach (ref pair; set.root.children) 249 visit(pair.set); 250 } 251 visit(this); 252 return seen.length; 253 }()); 254 } 255 256 /// Collect the names of all dimensions occurring in this tree. 257 DimName[] getDims() const 258 { 259 HashSet!DimName dims; 260 HashSet!MapSet seen; 261 void visit(MapSet set) 262 { 263 if (set is emptySet || set is unitSet || set in seen) 264 return; 265 seen.add(set); 266 dims.add(set.root.dim); 267 foreach (ref pair; set.root.children) 268 visit(pair.set); 269 } 270 visit(this); 271 return dims.keys; 272 } 273 274 /// Return all values for all dimensions occurring in this set. 275 /// The Cartesian product of these values would thus be a superset 276 /// of this set. 277 /// This is equivalent to, but faster than, calling `getDims` and 278 /// then `all` for each dim. 279 HashSet!DimValue[DimName] getDimsAndValues() const 280 { 281 if (this is emptySet) return null; 282 // Be careful to count the implicit nullValues on branches 283 // where a dim doesn't occur. 284 auto dims = getDims(); 285 auto dimIndices = OrderedSet!DimName(dims); 286 auto dimSeen = new bool[dims.length]; 287 HashSet!DimValue[DimName] result; 288 HashSet!MapSet seen; 289 void visit(MapSet set) 290 { 291 if (set is unitSet) 292 { 293 foreach (i, seen; dimSeen) 294 if (!seen) 295 result.require(dims[i], HashSet!DimValue.init).add(nullValue); 296 return; 297 } 298 299 if (set in seen) return; 300 seen.add(set); 301 auto pvalues = &result.require(set.root.dim, HashSet!DimValue.init); 302 foreach (ref pair; set.root.children) 303 pvalues.add(pair.value); 304 305 auto i = dimIndices.indexOf(set.root.dim); 306 dimSeen[i] = true; 307 foreach (ref pair; set.root.children) 308 visit(pair.set); 309 dimSeen[i] = false; 310 } 311 visit(this); 312 return result; 313 } 314 315 /// Combine two matrices together, returning their union. 316 /// If `other` is a subset of `this`, return `this` unchanged. 317 MapSet merge(MapSet other) const 318 { 319 if (this is emptySet) return other; 320 if (other is emptySet) return this; 321 if (this is unitSet && other is unitSet) return unitSet; 322 if (!root) return bringToFront(other.root.dim).merge(other); 323 324 this.assertDeduplicated(); 325 other.assertDeduplicated(); 326 return cache.merge.require(SetSetOp(this, other), { 327 other = other.bringToFront(root.dim); 328 329 if (root.children.length == 1 && other.root.children.length == 1 && 330 root.children[0].value == other.root.children[0].value) 331 { 332 // Single-child optimization 333 auto mergeResult = root.children[0].set.merge(other.root.children[0].set); 334 if (mergeResult !is root.children[0].set) 335 return MapSet(new immutable Node(root.dim, [Pair(root.children[0].value, mergeResult)])).deduplicate; 336 else 337 return this; 338 } 339 340 MapSet[DimValue] newChildren; 341 foreach (ref pair; root.children) 342 newChildren[pair.value] = pair.set; 343 344 bool modified; 345 foreach (ref pair; other.root.children) 346 newChildren.updateVoid(pair.value, 347 { 348 modified = true; 349 return pair.set; 350 }, 351 (ref MapSet oldSubmatrix) 352 { 353 auto mergeResult = oldSubmatrix.merge(pair.set); 354 if (oldSubmatrix !is mergeResult) 355 { 356 oldSubmatrix = mergeResult; 357 modified = true; 358 } 359 } 360 ); 361 362 if (!modified) 363 return this; 364 365 return MapSet(new immutable Node(root.dim, cast(immutable) newChildren)).deduplicate; 366 }()); 367 } 368 369 /// Return the difference between `this` and the given set. 370 /// If `other` does not intersect with `this`, return `this` unchanged. 371 MapSet subtract(MapSet other) const 372 { 373 if (this is emptySet) return this; 374 if (other is emptySet) return this; 375 if (this is unitSet && other is unitSet) return emptySet; 376 if (!root) return bringToFront(other.root.dim).subtract(other); 377 378 this.assertDeduplicated(); 379 other.assertDeduplicated(); 380 return cache.subtract.require(SetSetOp(this, other), { 381 other = other.bringToFront(root.dim); 382 383 if (root.children.length == 1 && other.root.children.length == 1 && 384 root.children[0].value == other.root.children[0].value) 385 { 386 // Single-child optimization 387 auto subtractResult = root.children[0].set.subtract(other.root.children[0].set); 388 if (subtractResult is emptySet) 389 return emptySet; 390 else 391 if (subtractResult !is root.children[0].set) 392 return MapSet(new immutable Node(root.dim, [Pair(root.children[0].value, subtractResult)])).deduplicate; 393 else 394 return this; 395 } 396 397 MapSet[DimValue] newChildren; 398 foreach (ref pair; root.children) 399 newChildren[pair.value] = pair.set; 400 401 bool modified; 402 foreach (ref pair; other.root.children) 403 if (auto poldSubmatrix = pair.value in newChildren) 404 { 405 auto subtractResult = poldSubmatrix.subtract(pair.set); 406 if (*poldSubmatrix !is subtractResult) 407 { 408 *poldSubmatrix = subtractResult; 409 if (subtractResult is emptySet) 410 newChildren.remove(pair.value); 411 modified = true; 412 } 413 } 414 415 if (!modified) 416 return this; 417 if (!newChildren.length) 418 return emptySet; 419 420 return MapSet(new immutable Node(root.dim, cast(immutable) newChildren)).deduplicate; 421 }()); 422 } 423 424 private MapSet lazyMap(scope MapSet delegate(MapSet) fn) const 425 { 426 // Defer allocation until the need to mutate 427 foreach (i, ref pair; root.children) // Read-only scan 428 { 429 auto newSet = fn(pair.set); 430 if (newSet !is pair.set) 431 { 432 auto newChildren = new Pair[root.children.length]; 433 // Known to not need mutation 434 newChildren[0 .. i] = root.children[0 .. i]; 435 // Reuse already calculated result 436 newChildren[i] = Pair(pair.value, newSet); 437 // Continue scan with mutation 438 foreach (j, ref pair2; root.children[i + 1 .. $]) 439 newChildren[i + 1 + j] = Pair(pair2.value, fn(pair2.set)); 440 return MapSet(new immutable Node(root.dim, cast(immutable) newChildren)).deduplicate; 441 } 442 } 443 return this; // No mutation necessary 444 } 445 446 /// "Unset" a given dimension, removing it from the matrix. 447 /// The result is the union of all sub-matrices for all values of `dim`. 448 MapSet remove(DimName dim) const 449 { 450 if (this is emptySet || this is unitSet) return this; 451 this.assertDeduplicated(); 452 return cache.remove.require(SetDimOp(this, dim), { 453 if (root.dim == dim) 454 { 455 MapSet result; 456 foreach (ref pair; root.children) 457 result = result.merge(pair.set); 458 return result; 459 } 460 return lazyMap(set => set.remove(dim)); 461 }()); 462 } 463 464 /// Set the given dimension to always have the given value, 465 /// collapsing (returning the union of) all sub-matrices 466 /// for previously different values of `dim`. 467 MapSet set(DimName dim, DimValue value) const 468 { 469 if (this is emptySet) return emptySet; 470 this.assertDeduplicated(); 471 auto removed = this.remove(dim); 472 if (value == nullValue) 473 return removed; 474 return MapSet(new immutable Node(dim, cast(immutable) [value : removed])).deduplicate; 475 } 476 477 /// Return a sub-matrix for all points where the given dimension has this value. 478 /// The dimension itself is not included in the result. 479 /// Note: if `dim` doesn't occur (e.g. because `this` is the unit set) and `value` is `nullValue`, 480 /// this returns the unit set (not the empty set). 481 MapSet slice(DimName dim, DimValue value) const 482 { 483 if (this is emptySet) return emptySet; 484 this.assertDeduplicated(); 485 foreach (ref pair; bringToFront(dim).root.children) 486 if (pair.value == value) 487 return pair.set; 488 return emptySet; 489 } 490 491 /// Return a subset of this set for all points where the given dimension has this value. 492 /// Unlike `slice`, the dimension itself is included in the result (with the given value). 493 MapSet get(DimName dim, DimValue value) const 494 { 495 if (this is emptySet) return emptySet; 496 this.assertDeduplicated(); 497 foreach (ref pair; bringToFront(dim).root.children) 498 if (pair.value == value) 499 return MapSet(new immutable Node(dim, [Pair(value, pair.set)])).deduplicate; 500 return emptySet; 501 } 502 503 /// Return all unique values occurring for a given dimension. 504 /// Unless this is the empty set, the return value is always non-empty. 505 /// If `dim` doesn't occur, it will be `[nullValue]`. 506 const(DimValue)[] all(DimName dim) const 507 { 508 // return bringToFront(dim).root.children.keys; 509 if (this is emptySet) return null; 510 if (this is unitSet) return [nullValue]; 511 if (root.dim == dim) return root.children.amap!(child => child.value); 512 this.assertDeduplicated(); 513 514 HashSet!DimValue allValues; 515 HashSet!MapSet seen; 516 void visit(MapSet set) 517 { 518 if (set is unitSet) 519 { 520 allValues.add(nullValue); 521 return; 522 } 523 if (set in seen) 524 return; 525 seen.add(set); 526 527 if (set.root.dim == dim) 528 foreach (ref pair; set.root.children) 529 allValues.add(pair.value); 530 else 531 foreach (ref pair; set.root.children) 532 visit(pair.set); 533 } 534 visit(this); 535 return allValues.keys; 536 } 537 538 /// Return a set which represents the Cartesian product between 539 /// this set and the given `values` across the specified 540 /// dimension. 541 MapSet cartesianProduct(DimName dim, DimValue[] values) const 542 { 543 if (this is emptySet) return emptySet; 544 if (values.length == 0) return emptySet; 545 this.assertDeduplicated(); 546 auto unset = this.remove(dim); 547 return MapSet(new immutable Node(dim, cast(immutable) values.map!(value => tuple(value, unset)).assocArray)).deduplicate; 548 } 549 550 /// Return a set which represents the Cartesian product between 551 /// this and the given set. 552 /// Duplicate dimensions are first removed from `this` set. 553 /// For best performance, call `big.cartesianProduct(small)` 554 MapSet cartesianProduct(MapSet other) const 555 { 556 if (this is emptySet || other is emptySet) return emptySet; 557 if (this is unitSet) return other; 558 if (other is unitSet) return this; 559 560 MapSet unset = this; 561 foreach (dim; other.getDims()) 562 unset = unset.remove(dim); 563 564 return other.uncheckedCartesianProduct(this); 565 } 566 567 // Assumes that the dimensions in `this` and `other` are disjoint. 568 private MapSet uncheckedCartesianProduct(MapSet other) const 569 { 570 if (this is unitSet) return other; 571 if (other is unitSet) return this; 572 573 this.assertDeduplicated(); 574 other.assertDeduplicated(); 575 576 return cache.cartesianProduct.require(SetSetOp(this, other), { 577 return lazyMap(set => set.uncheckedCartesianProduct(other)); 578 }()); 579 } 580 581 /// Refactor this matrix into one with the same data, 582 /// but putting the given dimension in front. 583 /// This will speed up access to values with the given dimension. 584 /// If the dimension does not yet occur in the set (or any subset), 585 /// it is instantiated with a single `nullValue` value. 586 /// The set must be non-empty. 587 MapSet bringToFront(DimName dim) const 588 { 589 assert(this !is emptySet, "Empty sets may not have dimensions"); 590 591 if (this is unitSet) 592 { 593 // We reached the bottom, and did not find `dim` along the way. 594 // Create it now. 595 return MapSet(new immutable Node(dim, [Pair(nullValue, unitSet)])).deduplicate; 596 } 597 598 if (dim == root.dim) 599 { 600 // Already at the front. 601 return this; 602 } 603 604 // 1. Recurse. 605 // 2. After recursion, all children should have `dim` at the front. 606 // So, just swap this layer with the next one. 607 608 this.assertDeduplicated(); 609 return cache.bringToFront.require(SetDimOp(this, dim), { 610 if (root.children.length == 1) 611 { 612 auto newSubmatrix = root.children[0].set.bringToFront(dim); 613 auto newChildren = newSubmatrix.root.children.dup; 614 foreach (ref pair; newChildren) 615 pair.set = MapSet(new immutable Node(root.dim, [Pair(root.children[0].value, pair.set)])).deduplicate; 616 return MapSet(new immutable Node(dim, cast(immutable) newChildren)).deduplicate; 617 } 618 619 Pair[][DimValue] submatrices; 620 foreach (ref pair; root.children) 621 { 622 auto newSubmatrix = pair.set.bringToFront(dim); 623 assert(newSubmatrix.root.dim == dim); 624 foreach (ref pair2; newSubmatrix.root.children) 625 submatrices[pair2.value] ~= Pair(pair.value, pair2.set); 626 } 627 Pair[] newChildren; 628 foreach (value, children; submatrices) 629 newChildren ~= Pair(value, MapSet(new immutable Node(root.dim, cast(immutable) children)).deduplicate); 630 return MapSet(new immutable Node(dim, cast(immutable) newChildren)).deduplicate; 631 }()); 632 } 633 634 /// Refactor this matrix into one with the same data, 635 /// but attempting to lower the total number of nodes. 636 MapSet optimize() const 637 { 638 if (this is emptySet || this is unitSet) return this; 639 this.assertDeduplicated(); 640 641 return cache.optimize.require(this, { 642 bool modified; 643 MapSet[DimValue] newChildren; 644 foreach (ref pair; root.children) 645 { 646 auto newMatrix = pair.set.optimize; 647 if (newMatrix !is pair.set) 648 { 649 modified = true; 650 assert(newMatrix.count == pair.set.count); 651 } 652 newChildren[pair.value] = newMatrix; 653 } 654 655 MapSet result = modified ? MapSet(new immutable Node(root.dim, cast(immutable) newChildren)).deduplicate : this; 656 657 foreach (ref pair; result.root.children) 658 if (pair.set.root) 659 { 660 auto optimized = result.bringToFront(pair.set.root.dim); 661 // { 662 // import std.stdio; 663 // writefln("Trying to optimize:\n- Old: %d : %s\n- New: %d : %s", 664 // result.root.totalNodes, result, 665 // optimized.root.totalNodes, optimized, 666 // ); 667 // } 668 return optimized.uniqueNodes < result.uniqueNodes ? optimized : result; 669 } 670 671 return result.deduplicate; 672 }()); 673 } 674 675 void toString(scope void delegate(const(char)[]) sink) const 676 { 677 import std.format : formattedWrite; 678 if (this is emptySet) 679 sink("{}"); 680 else 681 if (this is unitSet) 682 sink("{[]}"); 683 else 684 sink.formattedWrite!"%s"(*root); 685 } 686 687 hash_t toHash() const 688 { 689 return 690 this is emptySet ? 0 : 691 this is unitSet ? 1 : 692 root.toHash(); 693 } 694 695 bool opEquals(const typeof(this) s) const 696 { 697 if (root is s.root) 698 return true; 699 if (this is emptySet || this is unitSet || s is emptySet || s is unitSet) 700 return this is s; 701 return *root == *s.root; 702 } 703 } 704 705 unittest 706 { 707 import std.algorithm.sorting : sort; 708 709 alias M = MapSet!(string, int); 710 M m, n; 711 m = m.merge(M.unitSet.set("x", 1).set("y", 5)); 712 m = m.merge(M.unitSet.set("x", 1).set("y", 6)); 713 assert(m.all("x") == [1]); 714 assert(m.all("y").dup.sort.release == [5, 6]); 715 716 m = m.merge(M.unitSet.set("x", 2).set("y", 6)); 717 assert(m.get("x", 1).all("y").dup.sort.release == [5, 6]); 718 assert(m.get("y", 6).all("x").dup.sort.release == [1, 2]); 719 720 m = m.subtract(M.unitSet.set("x", 1).set("y", 6)); 721 assert(m.all("x").dup.sort.release == [1, 2]); 722 assert(m.all("y").dup.sort.release == [5, 6]); 723 assert(m.get("x", 1).all("y") == [5]); 724 assert(m.get("y", 6).all("x") == [2]); 725 726 m = M.emptySet; 727 m = m.merge(M.unitSet.set("x", 10).set("y", 20)); 728 m = m.merge(M.unitSet.set("x", 10).set("y", 21)); 729 m = m.merge(M.unitSet.set("x", 11).set("y", 21)); 730 m = m.merge(M.unitSet.set("x", 11).set("y", 22)); 731 auto o = m.optimize(); 732 assert(o.uniqueNodes < m.uniqueNodes); 733 734 m = M.emptySet; 735 assert(m.all("x") == []); 736 m = M.unitSet; 737 assert(m.all("x") == [0]); 738 m = m.merge(M.unitSet.set("x", 1)); 739 assert(m.all("x").dup.sort.release == [0, 1]); 740 741 m = M.unitSet; 742 assert(m.set("x", 1).set("x", 1).all("x") == [1]); 743 744 m = M.unitSet; 745 m = m.cartesianProduct("x", [1, 2, 3]); 746 m = m.cartesianProduct("y", [1, 2, 3]); 747 m = m.cartesianProduct("z", [1, 2, 3]); 748 assert(m.count == 3 * 3 * 3); 749 assert(m .all("x").dup.sort.release == [1, 2, 3]); 750 assert(m.set("z", 1).all("x").dup.sort.release == [1, 2, 3]); 751 assert(m.set("x", 1).all("z").dup.sort.release == [1, 2, 3]); 752 753 m = M.unitSet; 754 m = m.cartesianProduct("a", [1, 2, 3]); 755 m = m.cartesianProduct("b", [1, 2, 3]); 756 n = M.unitSet; 757 n = n.cartesianProduct("c", [1, 2, 3]); 758 n = n.cartesianProduct("d", [1, 2, 3]); 759 m = m.cartesianProduct(n); 760 assert(m.count == 3 * 3 * 3 * 3); 761 762 assert(M.unitSet != M.unitSet.set("x", 1)); 763 } 764 765 766 /// Allows executing a deterministic algorithm over all states in a given MapSet. 767 /// If a variable is not queried by the algorithm, states for all 768 /// variations of that variable are processed in one iteration. 769 struct MapSetVisitor(A, V) 770 { 771 alias Set = MapSet!(A, V); 772 Set set; 773 774 struct Var 775 { 776 A name; 777 const(V)[] values; 778 size_t pos; 779 } 780 Var[] stack; 781 V[A] singularValues, resolvedValues; // Faster than currentSubset.all(name)[0] 782 Set currentSubset; 783 784 this(Set set) 785 { 786 this.set = set; 787 foreach (dim, values; set.getDimsAndValues()) 788 if (values.length == 1) 789 singularValues[dim] = values.byKey.front; 790 } 791 792 /// Resets iteration to the beginning. 793 /// Equivalent to but faster than constructing a new MapSetVisitor 794 /// instance (`visitor = MapSetVisitor(visitor.set)`). 795 void reset() 796 { 797 currentSubset = Set.emptySet; 798 stack = null; 799 } 800 801 /// Returns true if there are more states to iterate over, 802 /// otherwise returns false 803 bool next() 804 { 805 if (set is Set.emptySet) 806 return false; 807 if (currentSubset is Set.emptySet) 808 { 809 // first iteration 810 } 811 else 812 while (true) 813 { 814 if (!stack.length) 815 return false; // All possibilities exhausted 816 auto last = &stack[$-1]; 817 last.pos++; 818 if (last.pos == last.values.length) 819 { 820 stack = stack[0 .. $ - 1]; 821 continue; 822 } 823 break; 824 } 825 826 currentSubset = set; 827 resolvedValues = null; 828 foreach (ref var; stack) 829 { 830 auto value = var.values[var.pos]; 831 currentSubset = currentSubset.get(var.name, value); 832 resolvedValues[var.name] = value; 833 } 834 return true; 835 } 836 837 /// Algorithm interface - get a value by name 838 V get(A name) 839 { 840 if (auto pvalue = name in resolvedValues) 841 return *pvalue; 842 if (auto pvalue = name in singularValues) 843 return *pvalue; 844 845 auto values = currentSubset.all(name); 846 auto value = values[0]; 847 resolvedValues[name] = value; 848 stack ~= Var(name, values, 0); 849 if (values.length > 1) 850 currentSubset = currentSubset.get(name, value); 851 return value; 852 } 853 854 /// Algorithm interface - set a value by name 855 void put(A name, V value) 856 { 857 currentSubset = currentSubset.set(name, value); 858 resolvedValues[name] = value; 859 } 860 861 /// Apply a function over every possible value of the given 862 /// variable, without resolving it (unless it's already resolved). 863 void transform(A name, scope void delegate(ref V value) fun) 864 { 865 if (auto pvalue = name in resolvedValues) 866 return fun(*pvalue); 867 868 currentSubset = currentSubset.bringToFront(name); 869 auto newChildren = currentSubset.root.children.dup; 870 foreach (ref child; newChildren) 871 fun(child.value); 872 currentSubset = Set(new immutable Set.Node(name, cast(immutable) newChildren)); 873 } 874 } 875 876 /// An algorithm which divides two numbers. 877 /// When the divisor is zero, we don't even query the dividend, 878 /// therefore processing all dividends in one iteration. 879 unittest 880 { 881 alias M = MapSet!(string, int); 882 M m = M.unitSet 883 .cartesianProduct("divisor" , [0, 1, 2]) 884 .cartesianProduct("dividend", [0, 1, 2]); 885 assert(m.count == 9); 886 887 auto v = MapSetVisitor!(string, int)(m); 888 M results; 889 int iterations; 890 while (v.next()) 891 { 892 iterations++; 893 auto divisor = v.get("divisor"); 894 if (divisor == 0) 895 continue; 896 auto dividend = v.get("dividend"); 897 v.put("quotient", dividend / divisor); 898 results = results.merge(v.currentSubset); 899 } 900 901 assert(iterations == 7); // 1 for division by zero + 3 for division by one + 3 for division by two 902 assert(results.get("divisor", 2).get("dividend", 2).all("quotient") == [1]); 903 assert(results.get("divisor", 0).count == 0); 904 } 905 906 unittest 907 { 908 import std.algorithm.sorting : sort; 909 910 alias M = MapSet!(string, int); 911 M m = M.unitSet.cartesianProduct("x", [1, 2, 3]); 912 auto v = MapSetVisitor!(string, int)(m); 913 v.next(); 914 v.transform("x", (ref int v) { v *= 2; }); 915 assert(v.currentSubset.all("x").dup.sort.release == [2, 4, 6]); 916 }