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