1 /** 2 * An `Enumap` maps each member of an enum to a single value. 3 * 4 * An `Enumap` is effectively a lightweight associative array with some benefits. 5 * 6 * You can think of an `Enumap!(MyEnum, int)` somewhat like a `int[MyEnum]`, 7 * with the following differences: 8 * 9 * - `Enumap!(K,V)` is a value type and requires no dynamic memory allocation 10 * 11 * - `Enumap!(K,V)` has a pre-initialized entry for every member of `K` 12 * 13 * - `Enumap!(K,V)` supports the syntax `map.name` as an alias to `map[K.name]` 14 * 15 * - `Enumap!(K,V)` supports array-wise operations 16 * 17 * Authors: Ryan Roden-Corrent ($(LINK2 https://github.com/rcorre, rcorre)) 18 * License: MIT 19 * Copyright: © 2015, Ryan Roden-Corrent 20 * 21 * Examples: 22 * Suppose you are building a good ol' dungeon-crawling RPG. 23 * Where to start? How about the classic 6 attributes: 24 * 25 * --- 26 * enum Attribute { 27 * strength, dexterity, constitution, wisdom, intellect, charisma 28 * }; 29 * 30 * struct Character { 31 * Enumap!(Attribute, int) attributes; 32 * } 33 * --- 34 * 35 * Lets roll some stats! 36 * 37 * --- 38 * Character hero; 39 * hero.attributes = sequence!((a,n) => uniform!"[]"(0, 20))().take(6); 40 * --- 41 * 42 * Note that we can assign directly from a range! 43 * Just like static array assignment, it will fail if the length doesn't match. 44 * 45 * We can access those values using either `opIndex` or `opDispatch`: 46 * 47 * --- 48 * if (hero.attributes[Attribute.wisdom] < 5) hero.drink(unidentifiedPotion); 49 * // equivalent 50 * if (hero.attributes.wisdom < 5) hero.drink(unidentifiedPotion); 51 * --- 52 * 53 * We can also perform binary operations between `Enumap`s: 54 * 55 * --- 56 * // note the convenient assignment from an associative array: 57 * Enumap!(Attribute, int) bonus = {Attribute.charisma: 2, Attribute.wisom: 1}; 58 * 59 * // level up! adds 2 to charisma and 1 to wisdom. 60 * hero.attributes += bonus; 61 * --- 62 * 63 * Finally, note that we can break the `Enumap` down into a range when needed: 64 * 65 * --- 66 * hero.attributes = hero.attributes.byValue.map!(x => x + 1); 67 * --- 68 * 69 * See the full documentation of `Enumap` for all the operations it supports. 70 * 71 */ 72 module enumap; 73 74 import std.algorithm : map; 75 import std.conv : to; 76 import std.meta : aliasSeqOf; 77 import std.range; 78 import std.traits : Unqual, EnumMembers; 79 import std.typecons : tuple; 80 81 /** 82 * A structure that maps each member of an enum to a single value. 83 * 84 * An `Enumap` is a lightweight alternative to an associative array that is 85 * useable when your key type is an enum. 86 * 87 * It provides some added benefits over the AA, such as array-wise operations, 88 * default values for all keys, and some nice `opDispatch` based syntactic 89 * sugar for element access. 90 * 91 * The key enum must be backed by an integral type and have 'default' numbering. 92 * The backing value must start at 0 and grows by 1 for each member. 93 * 94 * Params: 95 * K = The type of enum used as a key. 96 * The enum values must start at 0, and increase by 1 for each entry. 97 * V = The type of value stored for each enum member 98 */ 99 struct Enumap(K, V) 100 if(EnumMembers!K == aliasSeqOf!(EnumMembers!K.length.iota)) 101 { 102 private static immutable _keys = [EnumMembers!K]; 103 104 /// The number of entries in the `Enumap` 105 static immutable length = _keys.length; 106 107 /// Assuming Element consists of air, earth, water, and fire (4 members): 108 unittest { 109 static assert(Enumap!(Element, int).length == 4); 110 } 111 112 private V[length] _store; 113 114 /// Construct an Enumap from a static array. 115 this(V[length] values) { 116 _store = values; 117 } 118 119 /// 120 @nogc unittest { 121 auto elements = Enumap!(Element, int)([1, 2, 3, 4]); 122 123 assert(elements[Element.air] == 1); 124 assert(elements[Element.earth] == 2); 125 assert(elements[Element.water] == 3); 126 assert(elements[Element.fire] == 4); 127 } 128 129 /// Assign from a range with a number of elements exactly matching `length`. 130 this(R)(R values) if (isInputRange!R && is(ElementType!R : V)) { 131 int i = 0; 132 foreach(val ; values) { 133 assert(i < length, "range contains more values than Enumap"); 134 _store[i++] = val; 135 } 136 assert(i == length, "range contains less values than Enumap"); 137 } 138 139 /// 140 @nogc unittest { 141 import std.range : repeat; 142 Enumap!(Element, int) elements = 9.repeat(4); 143 assert(elements.air == 9); 144 assert(elements.earth == 9); 145 assert(elements.water == 9); 146 assert(elements.fire == 9); 147 } 148 149 /// An Enumap can be assigned from an array or range of values 150 void opAssign(T)(T val) if (is(typeof(typeof(this)(val)))) { 151 this = typeof(this)(val); 152 } 153 154 /// Assign an Enumap from a static array. 155 @nogc unittest { 156 Enumap!(Element, int) elements; 157 int[elements.length] arr = [1, 2, 3, 4]; 158 elements = arr; 159 160 with (Element) { 161 assert(elements[air] == 1); 162 assert(elements[earth] == 2); 163 assert(elements[water] == 3); 164 assert(elements[fire] == 4); 165 } 166 } 167 168 /// Assign an Enumap from a range 169 @nogc unittest { 170 import std.range : iota; 171 172 Enumap!(Element, int) elements; 173 174 with (Element) { 175 elements = iota(0, 4); 176 177 assert(elements[air] == 0); 178 assert(elements[earth] == 1); 179 assert(elements[water] == 2); 180 assert(elements[fire] == 3); 181 } 182 } 183 184 /** 185 * Access the value at the index specified by an enum member. 186 * 187 * Indexing returns a reference, so it can be used as a getter or setter. 188 */ 189 ref auto opIndex(K key) inout { 190 return _store[key]; 191 } 192 193 /// 194 @nogc unittest { 195 Enumap!(Element, int) elements; 196 elements[Element.fire] = 4; 197 assert(elements[Element.fire] == 4); 198 } 199 200 /** 201 * Access the value at the index specified by the name of an enum member. 202 * 203 * The value is returned by reference, so it can used for assignment. 204 * `map.name` is just syntactic sugar for `map[SomeEnum.name]`. 205 */ 206 ref auto opDispatch(string s)() inout { 207 enum key = s.to!K; 208 return this[key]; 209 } 210 211 /// 212 @nogc unittest { 213 Enumap!(Element, int) elements; 214 elements.water = 5; 215 assert(elements.water == 5); 216 } 217 218 /// Execute a foreach statement over (EnumMember, value) pairs. 219 int opApply(scope int delegate(K, const V) dg) const { 220 // declare the callee as @nogc so @nogc callers can use foreach 221 // oddly, this works even if dg is non-nogc... huh? 222 // I'm just gonna take this and run before the compiler catches me. 223 alias nogcDelegate = @nogc int delegate(K, const V); 224 auto callme = cast(nogcDelegate) dg; 225 226 int res = 0; 227 228 foreach(key ; _keys) { 229 res = callme(key, this[key]); 230 if (res) break; 231 } 232 233 return res; 234 } 235 236 /// foreach iterates over (EnumMember, value) pairs. 237 @nogc unittest { 238 const auto elements = enumap(Element.water, 4, Element.air, 3); 239 240 foreach(key, value ; elements) { 241 assert( 242 key == Element.water && value == 4 || 243 key == Element.air && value == 3 || 244 value == 0); 245 } 246 } 247 248 /// Execute foreach over (EnumMember, ref value) pairs to modify elements. 249 int opApply(scope int delegate(K, ref V) dg) { 250 // declare the callee as @nogc so @nogc callers can use foreach 251 // oddly, this works even if dg is non-nogc... huh? 252 // I'm just gonna take this and run before the compiler catches me. 253 alias nogcDelegate = @nogc int delegate(K, ref V); 254 auto callme = cast(nogcDelegate) dg; 255 256 int res = 0; 257 258 foreach(key ; _keys) { 259 res = callme(key, this[key]); 260 if (res) break; 261 } 262 263 return res; 264 } 265 266 /// foreach can modify values by ref 267 @nogc unittest { 268 Enumap!(Element, int) elements; 269 270 foreach(key, ref value ; elements) { 271 if (key == Element.air) value = 4; 272 else if (key == Element.water) value = 2; 273 } 274 275 assert(elements.air == 4); 276 assert(elements.water == 2); 277 } 278 279 // make sure you can foreach with a non-nogc delegate 280 unittest { 281 foreach(key, ref value ; enumap(Element.water, "here comes...")) 282 value ~= "a spurious allocation!"; 283 } 284 285 /// Apply an array-wise operation between two `Enumap`s. 286 auto opBinary(string op)(inout typeof(this) other) const 287 if (is(typeof(mixin("V.init"~op~"V.init")) : V)) 288 { 289 Unqual!(typeof(this)) result; 290 291 foreach(member ; _keys) { 292 result[member] = mixin("this[member]"~op~"other[member]"); 293 } 294 295 return result; 296 } 297 298 /// 299 @nogc unittest { 300 immutable base = enumap(Element.water, 4, Element.air , 3); 301 immutable bonus = enumap(Element.water, 5, Element.fire, 2); 302 303 immutable sum = base + bonus; 304 immutable diff = base - bonus; 305 immutable prod = base * bonus; 306 307 assert(sum.water == 4 + 5); 308 assert(sum.air == 3 + 0); 309 assert(sum.fire == 0 + 2); 310 assert(sum.earth == 0 + 0); 311 312 assert(diff.water == 4 - 5); 313 assert(diff.air == 3 - 0); 314 assert(diff.fire == 0 - 2); 315 assert(diff.earth == 0 - 0); 316 317 assert(prod.water == 4 * 5); 318 assert(prod.air == 3 * 0); 319 assert(prod.fire == 0 * 2); 320 assert(prod.earth == 0 * 0); 321 } 322 323 /// 324 unittest { 325 auto inventory = enumap( 326 ItemType.junk , [ "Gemstone" ], 327 ItemType.normal, [ "Sword", "Shield" ], 328 ItemType.key , [ "Bronze Key" ]); 329 330 auto loot = enumap( 331 ItemType.junk , [ "Potato" ], 332 ItemType.normal, [ "Potion" ], 333 ItemType.key , [ "Skeleton Key" ]); 334 335 inventory ~= loot; 336 337 assert(inventory.junk == [ "Gemstone", "Potato" ]); 338 assert(inventory.normal == [ "Sword", "Shield" , "Potion" ]); 339 assert(inventory.key == [ "Bronze Key" , "Skeleton Key" ]); 340 } 341 342 /// Perform an in-place operation. 343 auto opOpAssign(string op)(inout typeof(this) other) 344 if (is(typeof(this.opBinary!op(other)) : typeof(this))) 345 { 346 this = this.opBinary!op(other); 347 } 348 349 /// 350 @nogc unittest { 351 auto base = enumap(Element.water, 4, Element.air , 3); 352 const bonus = enumap(Element.water, 5, Element.fire, 2); 353 354 base += bonus; 355 356 assert(base.water == 4 + 5); 357 assert(base.air == 3 + 0); 358 assert(base.fire == 0 + 2); 359 assert(base.earth == 0 + 0); 360 361 base -= bonus; // cancel out the previous addition 362 363 assert(base.water == 4); 364 assert(base.air == 3); 365 assert(base.fire == 0); 366 assert(base.earth == 0); 367 368 base *= bonus; 369 assert(base.water == 4 * 5); 370 assert(base.air == 3 * 0); 371 assert(base.fire == 0 * 2); 372 assert(base.earth == 0 * 0); 373 } 374 375 /// Perform a unary operation on each entry. 376 auto opUnary(string op)() const 377 if (is(typeof(mixin(op~"V.init")) : V)) 378 { 379 V[length] result = mixin(op~"_store[]"); 380 return typeof(this)(result); 381 } 382 383 @nogc unittest { 384 immutable elements = enumap(Element.water, 4); 385 assert((-elements).water == -4); 386 } 387 388 /// Get a range iterating over the members of the enum `K`. 389 auto byKey() const { return _keys; } 390 391 @nogc unittest { 392 import std.range : only; 393 import std.algorithm : equal; 394 395 Enumap!(Element, int) e; 396 with (Element) { 397 assert(e.byKey.equal(only(air, earth, water, fire))); 398 } 399 } 400 401 /// Get a range iterating over the stored values. 402 auto byValue() inout { return _store[]; } 403 404 /// you can use byValue to perform range based operations on the values: 405 @nogc unittest { 406 import std.range : iota; 407 import std.algorithm : map, equal; 408 409 const Enumap!(Element, int) e1 = iota(0, 4); 410 const Enumap!(Element, int) e2 = e1.byValue.map!(x => x + 2); 411 assert(e2.byValue.equal(iota(2, 6))); 412 } 413 414 /// `byValue` supports ref access: 415 @nogc unittest { 416 with (Element) { 417 auto elements = enumap(air, 1, water, 2, fire, 3, earth, 4); 418 foreach(ref val ; elements.byValue) ++val; 419 assert(elements == enumap(air, 2, water, 3, fire, 4, earth, 5)); 420 } 421 } 422 423 /** 424 * Return a range of (EnumMember, value) pairs. 425 * 426 * Note that byKeyValue does _not_ support modifying the underlying values by 427 * reference. 428 * For that, you should just use foreach directly (see `opApply`). 429 */ 430 auto byKeyValue() const { 431 alias StoreType = typeof(_store); 432 433 struct Result { 434 private StoreType _source; 435 private int _idx; 436 437 bool empty() { return _idx >= length; } 438 auto front() { return tuple(_keys[_idx], _source[_idx]); } 439 void popFront() { ++_idx; } 440 } 441 442 return Result(_store); 443 } 444 445 /// 446 @nogc unittest { 447 import std.typecons : tuple; 448 import std.algorithm : map; 449 450 immutable elements = enumap(Element.water, 4, Element.air, 3); 451 452 auto pairs = elements.byKeyValue.map!(pair => tuple(pair[0], pair[1] + 1)); 453 454 foreach(key, value ; pairs) { 455 assert( 456 key == Element.water && value == 5 || 457 key == Element.air && value == 4 || 458 value == 1); 459 } 460 } 461 } 462 463 /** 464 * Construct an `Enumap` from a sequence of key/value pairs. 465 * 466 * Any values not specified default to `V.init`. 467 */ 468 @nogc auto enumap(T...)(T pairs) if (T.length >= 2 && T.length % 2 == 0) { 469 alias K = T[0]; 470 alias V = T[1]; 471 472 Enumap!(K, V) result; 473 474 // pop a key/vaue pair, assign it to the enumap, and recurse until empty 475 void helper(U...)(U params) { 476 static assert(is(U[0] == K), "enumap: mismatched key type"); 477 static assert(is(U[1] == V), "enumap: mismatched value type"); 478 479 auto key = params[0]; 480 auto val = params[1]; 481 482 result[key] = val; 483 484 static if (U.length > 2) { 485 helper(params[2..$]); 486 } 487 } 488 489 helper(pairs); 490 return result; 491 } 492 493 /// 494 @nogc unittest { 495 with (Element) { 496 auto elements = enumap(air, 1, earth, 2, water, 3); 497 498 assert(elements[air] == 1); 499 assert(elements[earth] == 2); 500 assert(elements[water] == 3); 501 assert(elements[fire] == 0); // unspecified values default to V.init 502 } 503 } 504 505 version (unittest) { 506 // define some enums for testing 507 private enum Element { air, earth, water, fire }; 508 private enum ItemType { junk, normal, key }; 509 } 510 511 // make sure the readme examples work: 512 unittest { 513 import std.algorithm, std.range, std.random; 514 515 enum Attribute { 516 strength, dexterity, constitution, wisdom, intellect, charisma 517 } 518 519 Enumap!(Attribute, int) attributes; 520 assert(attributes.wisdom == 0); // default value check 521 522 attributes[Attribute.strength] = 10; 523 524 // generate is not implemented for GDC (maybe others too?) 525 static if (is(typeof(std.range.generate))) 526 attributes = generate!(() => uniform!"[]"(1, 20)).take(6); 527 528 // make sure accessors compile: 529 if (attributes[Attribute.wisdom] < 5) { } 530 if (attributes.wisdom < 5) { } 531 532 // key/value constructor 533 auto bonus = enumap(Attribute.charisma, 2, Attribute.wisdom, 1); 534 535 // opBinary 536 attributes += bonus; 537 538 // nogc test 539 void donFancyHat(int[Attribute] attrs) { attrs[Attribute.charisma] += 1; } 540 @nogc void donFancyHat2(Enumap!(Attribute, int) attrs) { attrs.charisma += 1; } 541 } 542 543 // The enum members must have ascending values in sequence, from 0 on. 544 unittest { 545 enum Acceptable { zero = 0, one = 1, two = 2, three = 3 } 546 enum WrongOrder { zero = 0, one = 2, two = 1, three = 3 } 547 enum WrongStart { zero = 3, one = 4, two = 5, three = 6 } 548 assert(__traits(compiles, enumap!(Acceptable, int))); 549 assert(!__traits(compiles, enumap!(WrongOrder, int))); 550 assert(!__traits(compiles, enumap!(WrongStart, int))); 551 } 552 553 // constness tests: 554 unittest { 555 auto mmap = enumap(Element.air, 2, Element.water, 4); 556 const cmap = enumap(Element.air, 2, Element.water, 4); 557 immutable imap = enumap(Element.air, 2, Element.water, 4); 558 559 // value access permitted on all 560 static assert(__traits(compiles, mmap[Element.air] == 2)); 561 static assert(__traits(compiles, cmap[Element.air] == 2)); 562 static assert(__traits(compiles, imap[Element.air] == 2)); 563 static assert(__traits(compiles, mmap.air == 2)); 564 static assert(__traits(compiles, cmap.air == 2)); 565 static assert(__traits(compiles, imap.air == 2)); 566 567 // value setters only permitted if mutable 568 static assert( __traits(compiles, { mmap[Element.air] = 2; })); 569 static assert(!__traits(compiles, { cmap[Element.air] = 2; })); 570 static assert(!__traits(compiles, { imap[Element.air] = 2; })); 571 static assert( __traits(compiles, { mmap.air = 2; })); 572 static assert(!__traits(compiles, { cmap.air = 2; })); 573 static assert(!__traits(compiles, { imap.air = 2; })); 574 575 // readonly foreach permitted on all 576 static assert(__traits(compiles, { foreach(k,v ; mmap) {} })); 577 static assert(__traits(compiles, { foreach(k,v ; cmap) {} })); 578 static assert(__traits(compiles, { foreach(k,v ; imap) {} })); 579 580 // foreach with modification permitted only if mutable 581 static assert( __traits(compiles, { foreach(k, ref v ; mmap) {++v;} })); 582 static assert(!__traits(compiles, { foreach(k, ref v ; cmap) {++v;} })); 583 static assert(!__traits(compiles, { foreach(k, ref v ; imap) {++v;} })); 584 585 // opBinary permitted on all 586 static assert(__traits(compiles, { immutable res = mmap + mmap; })); 587 static assert(__traits(compiles, { immutable res = cmap + cmap; })); 588 static assert(__traits(compiles, { immutable res = imap + imap; })); 589 static assert(__traits(compiles, { immutable res = mmap + imap; })); 590 static assert(__traits(compiles, { immutable res = imap + mmap; })); 591 592 // opBinaryAssign permitted only if mutable 593 static assert( __traits(compiles, { mmap += mmap; })); 594 static assert( __traits(compiles, { mmap += cmap; })); 595 static assert( __traits(compiles, { mmap += imap; })); 596 static assert(!__traits(compiles, { cmap += mmap; })); 597 static assert(!__traits(compiles, { cmap += cmap; })); 598 static assert(!__traits(compiles, { imap += imap; })); 599 600 // opUnary permitted on all 601 static assert(__traits(compiles, { -mmap; })); 602 static assert(__traits(compiles, { -cmap; })); 603 static assert(__traits(compiles, { -imap; })); 604 605 // opUnary permitted on all 606 static assert(__traits(compiles, { -mmap; })); 607 static assert(__traits(compiles, { -cmap; })); 608 static assert(__traits(compiles, { -imap; })); 609 610 // byKey permitted on all 611 static assert(__traits(compiles, { mmap.byKey; })); 612 static assert(__traits(compiles, { cmap.byKey; })); 613 static assert(__traits(compiles, { imap.byKey; })); 614 615 // byValue permitted on all 616 static assert(__traits(compiles, { foreach(v ; mmap.byValue) {} })); 617 static assert(__traits(compiles, { foreach(v ; cmap.byValue) {} })); 618 static assert(__traits(compiles, { foreach(v ; imap.byValue) {} })); 619 620 // byValue with ref assignment permitted only if mutable 621 static assert( __traits(compiles, { foreach(ref v ; mmap.byValue) ++v; })); 622 static assert(!__traits(compiles, { foreach(ref v ; cmap.byValue) ++v; })); 623 static assert(!__traits(compiles, { foreach(ref v ; imap.byValue) ++v; })); 624 625 // byKeyValue permitted on all 626 static assert(__traits(compiles, { foreach(k, v ; mmap.byKeyValue) {} })); 627 static assert(__traits(compiles, { foreach(k, v ; cmap.byKeyValue) {} })); 628 static assert(__traits(compiles, { foreach(k, v ; imap.byKeyValue) {} })); 629 }