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