Elegantly generate data in bulk with Ruby Enumerators and Arrays

If there's one way in which Ruby differentiates itself from other programming languages, it's in offering options to write code that's both concise and highly intention-revealing, all at the same time. In these two class RubyTapas videos—now free for all!—you can see myself and James Edward Gray II refactor from merely good code to tight, idiomatic, expressive Ruby code. Along the way you'll learn a novel use of Ruby Enumerators (not to be confused with Enumerable!) and a feature of the Array class you might not be familiar with.

First off, here's me showing how to turn some data-generation code into an expressive one-liner.

Ruby master (and friend of mine) James Gray saw that video, and knew he could do better. Here he is demonstrating how to use the full power of Ruby's Array class to express the same idea even more concisely.

The following is a transcript of the first video.

It's the little things, right?

Let's say we need a list of random coupon codes. We can get a random code using the SecureRandom module.

require "securerandom"

SecureRandom.hex(8)             # => "5ac7fd100d75260a"

Say we need 200 of these codes. In Ruby, when we want to do something 200 times, we don't typically use a general-purpose loop. Instead, we use the #times method on Integer.

200.times do
  # ...

OK, so we need to fill up an array with 200 random codes. We'll instantiate an empty array, and then fill it up within the loop.

require "securerandom"
codes = []
200.times do
  codes << SecureRandom.hex(8)

This is good code. Any reasonable programmer would be satisfied with this code.

…but I'm never satisfied. I always want to find a more concise-yet-expressive way to state something. And, I never like to separate the declaration of a variable and its initialization if I can avoid it. If someone has to move this loop someday, they'll have to be sure to grab the line above it as well. It's a little thing… but little things add up over time.

I'd prefer to assign the result of this loop directly to the codes variable. But that doesn't work as it is written, because the #times method just returns the number.

require "securerandom"
codes = 200.times do
codes                           # => 200

I guess the method we want here is #map, also known as #collect. #map adds the outcome of each iteration of the block to a result array. But integers don't have a #map method.

require "securerandom"
codes = 200.map do
codes                           # => 
# ~> -:2:in `<main>': undefined method `map' for 200:Fixnum (NoMethodError)

Are we stuck? Never! Let's go back to the #times method. What do we get when we don't pass it a block?

require "securerandom"
200.times                 # => #<Enumerator: 200:times>

Ah! Our old friend Enumerator. We've met Enumerator before, and we know that it implements all the methods of any Enumerable object. Including #map!

This gives us our solution: we send #times to the number, and then #map to resulting enumerator. Within the #map we generate our codes.

require "securerandom"
codes = 200.times.map { SecureRandom.hex(8) }
codes # => ["119d04e7ec93b9a2", "273c98e5069774a0", "c5b76deb40c44135", "a10fe7f7eac91030", "a8320bc6b5d6a696", "7791b9d82180a265", "36f43133e5fe1c35", "8b7614471e790f99", "f903edbc999a2e8b", "945a4d6dc44cd64e", "9ededf55c7796fa1", "0e6b2f60817c45b1", "22f3fb3e91ec8d8a", "781446082cbd3ef7", "97a09223fcd64d87", "e08206476599b7ec", "9a999406d62348b3", "b5ee19d728efda06", "99080883506117fc", "14ce485e5b31cf16", "eb6fd21f0ad89be0", "2b25601f4d28942e", "608482c8abaf4fcd", "bbe7aafe3fe77984", "1b83810a86ff24da", "8c0dcb19ed4ff501", "c93c71aac0571c49", "d0fcc0da4a045e23", "6e132e4929b89bdb", "b822b8bbfbda977c", "1a34533d9bc7da03", "62623e098bff03f3", "eaa857b764fbe8fa", "b313a8919c28a212", "f13f84c5ea7ede28", "8ac992c0dc4fc513", "69f820081febdaa5", "97699914a5cf8b35", "f20fd0db248849d8", "0cf9de22a4f8de82", "5563962ff7c38858", "b6fde887bab45c72", "c43a772795f15621", "b97f56f092f019f8", "b5f9726b6b31900c", "a6d682ad395f6b22", "260b1bba8592f940", "4041848b59b039e7", "75fd58aef747bf68", "db3da6696ee31da9", "713d1c170b016b04", "8ae31994d343d504", "6bfb7644e74d3b09", "ffd53ef2394ee440", "74ba4dc0205c73d0", "74bc1407993d9f9d", "14143836050999d2", "f85310d32ebfff84", "3a762f1406cbfdcf", "7abe983a96d74dc8", "2f4d5eb3c8299164", "77c68a75dca2a7a2", "3b995a1596cde2ae", "749e74bab33af935", "4bb71d45d6eb55b7", "3c74199e35e5b333", "d095d11574440e5a", "62f42ccd8cb738fd", "0d0a16dc0caf2622", "8ce078b8e0a303b6", "ba37776cf855a8b0", "e6cfd5c3b88c9fda", "93713535ba0e1171", "56493f7fa08334dd", "81d6fc3366df448b", "b60b01f77bb7dde1", "b98b6c1965260b3d", "4b5b7d29cc2f95da", "67a7999bf6e59156", "d53107107a4afcde", "d7b7d5c5513f40c9", "925a6146f2f035ce", "60597d5ec01273d3", "d979967c7866e7d2", "d6096955f81bddd7", "e0076c50ce0c59e5", "941a436e44c168cb", "b4b8bf411b746cf4", "a99db4a57b14a106", "abd9682835a12323", "90d64478f916d60f", "533ebb01be319457", "c3e50eef09b46b26", "c6fda73085567019", "aea1d08759b018c9", "1b01db77bc070dd7", "c6e3841d8c59eec8", "7cb7b8134f476861", "5d2c8335e9310561", "19d2d51f0058d049", "2de402dd500528c5", "e3ac62c8468a4519", "09e9e0bc3c7e542c", "8f75e383555724d7", "9b0bbb0d4ed78fdb", "bdf2cf9875d1564e", "d4f4656ae9236736", "545c00f18d95495e", "8213038b837b403e", "dde71261e49c89b7", "50455693ccdfec18", "4480c7ec31650a94", "e39a6ef088b48cc0", "edb7c225287651ac", "943714013665488a", "4bb6185072cc62a5", "b261f288c7be9165", "845475e687127757", "71d3578c520bccc4", "1a1fa7413a9a541a", "9b2e8af5650486de", "8d33577fdb509a39", "00157c9ef7317419", "b863912acc452703", "b9d960e805a36786", "4fc95a35ff4a0e29", "eb2169b69ca14999", "814d5f11ff15f544", "244d8838035312f2", "403af72f87ecfaa6", "9d5304ffa1ed43c8", "fde71573dbbd0903", "7f1ab7b4aba5218a", "824c3172f2ab4a65", "f973b51a3633d24f", "2f70f34fe9c8e064", "913a7506bde8252a", "4adb950420e47bb0", "9e6176761e690084", "a23838e6a818a16c", "bd15cab632a1eb47", "18254fbeb41506ae", "cd372fcc3f50146c", "5ff3fba6a200dd12", "87afd6ea6f3d3198", "752a2b9aeb24559d", "ac149bbe061abf18", "f6b243c4c7430935", "fd150a5ccde10da2", "38e455c1b09bdcd7", "a88a43735002cda1", "0020ec4e1828731a", "fc3151610d59b826", "49269e98f2f77c23", "1ddc454c027b5736", "ad1a329e5cd8fbc9", "c1a4f922dd484c30", "91b8a3e33ad6d8ec", "7d527961cc9170e8", "5ada8837bb40579c", "09d74ae5744e69f5", "beefbb77de8c5ae0", "92c58a5cb708b5fc", "d1764638b86881d5", "cd1b216304d056d5", "cad96187345ef9fe", "59e7b040a7204b23", "849aa179f17cb401", "e7c4d72f0ec244a8", "69f2fe2d0a67c26c", "c47aac51983e2a83", "90608bec99cadbb0", "9b610396db0f806f", "2e70a125273ca5be", "b9ee7804abad2f02", "b46f4b59b8b7f329", "113d40183898b09c", "51441171f37eb2e2", "be4178e776dc85b0", "c40183cd503b5dc5", "a11eed0eadc21395", "50def372b5cebebc", "fe7bd1c216247afe", "22de561128465598", "cde405e7c1c925d7", "5c8f643fed316ae0", "cd79574086017472", "efc9357c95e5e262", "b819eb93cd05a807", "99b9bdf951fcfc94", "73847c9b68ee9b38", "d51b70a71f95e1a0", "ba9a1c4bf3eb9c5b", "52095ba508e95974", "59498524365812b7", "0ebd3fe1a85d635c", "353b5c6a39ab62e6", "ca9c1feeebee6cb7", "83e3b591304ca663", "9b79bf17a693337a"]

And there it is: a list of codes declared and initialized all in a single line.

Just one more thing: While I think this is a beautifully elegant line of code, the use of the term “map” in this context feels a little bit weird. Fortunately, as I mentioned before, Ruby has another name for map: “collect”. While in most cases I prefer the “map” terminology, in this particular instance I feel like “collect” reads a little bit more clearly, since we are “collecting” a set of codes.

require "securerandom"
codes = 200.times.collect { SecureRandom.hex(8) }
codes # => ["6e880bdddcf829f6", "bd4eee889cf31a09", "9d303c8d89c0ee35", "d8f3a948d83a7cf0", "b003c3618993c01c", "9914b12e169ab3d3", "98b8f6496246d474", "c59444ecf75782e2", "0195eb038239e799", "146481a575d429ab", "5a0485808d343298", "6340791d29aa3755", "35cb1ea5e6900b1e", "e3ee7a2794230eea", "646e8f88967be2fe", "cadb8f06f09c7f6a", "55900572f55c2773", "b045bbcfcbb7f161", "37d6272bd8821ce1", "2fd9584efa0e7f04", "2215ce4571107115", "4f748a7e3f511c3e", "b044a658fbea45ec", "304b891458f77f2a", "bf736fe57487701e", "3d1c0a1a8274241d", "5916190ac9e64246", "42846f059501b99e", "e03b5ec8b7b66814", "662f1e2fd67a972f", "94e747351bd2bee4", "6499c698aeb9bc60", "fbab39ce565b6bdb", "f0bb9182d291d3c1", "ca5095713f92863d", "71a6bafa68794784", "83f44e7f6c2d6443", "aa78a70975f94dfc", "a07669b4391cdbc0", "637d711ec64d7037", "a92c6d8b0c245235", "162406e89fe9eef3", "d3192e1e1f89d31b", "bb4efd67fd33a188", "c2a06dc426ac629f", "a477430f75fba41d", "4ad4f4aa3fa6918f", "5baf29a55bd8bd11", "1b06c51149b0c48e", "8c4cc80a728b78b2", "95213be2ebd2fae9", "5317eb566ef35e7f", "712cd8c59d16ac09", "b87c9c95fe6e2350", "ea541fb4a998c0ee", "d9c8fe755fbf3dc9", "2c716df0c02f665e", "687a56f7dab28466", "ffe96d2ec1541f96", "fd08cd987baf605f", "2d0d8a911799d876", "2b0a15cec1594d94", "430b75c0d6934bff", "6725e729dde0f75d", "9bbb1bc017f6e827", "f1e9cf0768011e1e", "548648683353c05d", "191ce320a8fbfc52", "20923177c8bf15b4", "070db48d51cad00d", "cf5c91e5d514320d", "762619a71c2ac396", "607cc04d65644041", "a60d86d680d110fc", "ee870baa0ae8fbb2", "74e52df15aa13eed", "d60227e5231f20ff", "d6822640049411df", "94dc22674cf4ee4b", "89c6e59371238f9b", "ee838946c7ac3639", "a8bf2209d939b843", "3a07c222fe4e4839", "b4e93a0962f7b45d", "264a7e7b43ad3973", "42cfb6067f9ece87", "51937337b4b986de", "25fc8b7f63b17522", "9dcd3a475212ee60", "085d49ebb9fe2554", "75b22cfbf946ba82", "547d89413fce0485", "1185e1b22efd5564", "e56167a7d3faf561", "4dbab8df8202b419", "e11e02d08ae9eb48", "28988298147ef8a3", "8b12641f71a4360b", "4cbe0083222bb49e", "7680399fe023e932", "4adff22c0e0ec503", "75bac1c0d8b6a519", "de7326346315ce43", "4609b770677685da", "ac60fa432a5eb270", "1c246aeeda86062d", "c896744007355059", "34fae349a5de4db0", "0cf3337cf1f27457", "795ef69da68bbaca", "93dcd062d62fbed8", "93a10a9ab63ed224", "81a4af3c0dc51b34", "57065b2166f817da", "23a540b6647d987b", "82e4d54283723752", "43dc128786e4a1f9", "4586ee2bfcd52ff3", "84f9b573ea0b6a61", "fc352710f34a2ecd", "23620a4d2be6bed6", "e5e80990f11f76b6", "65e9811a22b31a93", "7e7095a2c896e281", "1fa9dc2ab97161fd", "eaa29472ff4ae1bf", "968d1620a9e45bb0", "11390b03961cc277", "836dd8346c0af910", "0475107aab6a155d", "de0515ddd78a5163", "114915988b69ff18", "0873b1363a67f8ec", "271214f9245ba75d", "6bb110cef5e1c090", "9c1615faf1723c86", "ca38b43f826401a0", "8329e79da7478c63", "5faa6dcce22dedaa", "989a6a0b99050cb9", "0ba06f69594d14a6", "904348ca0753c24d", "24df60f8d0f4d0be", "b1346bb308443d39", "0a53be2a592c4666", "1cd1a97174f6bb7a", "249413026f266194", "8a21bc9d3ce29351", "8bfe29b6bdaafa2d", "098be5314306d4ef", "d36dbd9f688a5e53", "1ddd03901dfafe4c", "e5bc54c2084dfb52", "1d40b7c8982e4762", "65daab0b630371e4", "f1739c750ab2fd5e", "9c40c71006c4065b", "046bb15bcb66a8fd", "6e4671ba5329d750", "1d83e9f8f069b92b", "c5340864812fbe43", "1218ec1b41d4fb49", "2488aad7e35b0b9a", "692cd56c06459d13", "1eee2f2f0fbf9484", "b4bd33ee9db25514", "ba2800d10af8d2f9", "13a6fbee5074e9ce", "810fe11059cfc198", "2739d1d2faf732fe", "02c7e8acfe799bd2", "41844cc36b4d79be", "ffeb593d541f4500", "c78acbcacd303ddf", "7467470f10b2e59b", "ad1f4632c876198e", "092f7b24e394dc92", "629193b19454a5bf", "4b335eb8f461ae22", "faf867aeadd7ea40", "ea32a6b9c0e8a484", "4e5ae8c2d880b2b8", "a1b4bc9a8cf30d3a", "afc3ce4a113165fe", "99b9e6cc8ff145f3", "4d84dd0f1ae47ae5", "f828c1ba14dd5c4c", "aed49d3c0280567d", "517b905066dc63d0", "ddcc850377c29fba", "28658a075906df3f", "30abbb7a50c4df7e", "484e670cbfa38f0b", "fdc94ba2cb2c6b56", "0c5de2e0dc99b461", "9788c87314d37c24", "477605ec2b7273e2", "deced44f215fe1d9", "049306d7ee1a82dd", "784d9036f49209c5"]

And with that, I think I'm done for now. Was this tiny refactoring worth the time and thought I put into it? Well, considered in terms of this single instance of code, perhaps not. But from now on whenever I need to generate N unique instances of something, I'll remember this idiom, and I'll write this tight, meaningful, self-contained one-liner instead of four lines of code. It's a little thing, but I think minding the little things is one of the ways we keep our code clean.

Happy hacking!

Note that James' video doesn't have a transcript. You'll just have to watch it!