I was recently doing a challenge on Exercism, on the Ruby track, and I struggled a lot, but when I ended up with a final solution, I was amazed at ...
For further actions, you may consider blocking this person and/or reporting abuse
Instead of:
You can just write:
I wondered about that. That totally makes sense. Thanks!
As yet another alternative approach... Taking either of your basic ideas (either storing all names in an
Array
, or anEnumerator
, or keeping a track oftaken_names
), you could use the regexp-examples ruby gem that I wrote a few years back to abstract the problem of "getting a valid-formatted robot name" -- for example:Then, if you have different robots with other naming rules, the rest of the cost would work out of the box. (And as a bonus, this regex will likely already exist in your code, to validate that the robot name is ok!)
Hi Tom! Thanks for sharing! How does this Gem handle collisions? One of the test cases generates all 676,000 robot names, so if calling
random_example
provides duplicates, we run into the same "theoretical infinite time complexity" issue, callingrandom_example
repeatedly until it provides the one robot name we haven't encountered yet.You could use
Regexp#random_example
in conjunction with thetaken_names
, as per your first solution in this blog post.Or -- with the potential performance issue of storing a large array in ruby memory (as with all your other examples that use
to_a
!) -- you could also useRegexp#examples
to end up with very similar solutions. (See the documentation.) For example:.
...Note that this would be a bad idea if the pattern got longer, so the number of possible results was much larger. All of your examples that use
to_a
, just like myRegexp#examples
code above, would soon freeze the system as the array length grows exponentially.Using
Regexp#random_example
- similar to your original implementation - would scale fine though.I really enjoyed reading this journey and seeing the trade offs you had to face. Thanks for sharing!
I agree w/ Phil.
Further, I've had the same cry sessions for similar reasons: I worked hard and long to do it one way and here comes a Ruby method or something that not only does it easier but with less lines of code.
🤦🏾♀️
Glad you liked it!
You can fix this by swapping the element with the last element, and then popping off the last element, which is always cheap:
This only works because you don't actually care about the order of values in
@@possible_names
!Another much more involved approach would be some kind of sparse Fenwick tree. That way, startup time is instantaneous, and you only use lots of memory when you've roughly used up every-other ID.
Woah cool! I’ll look into that. Thanks for the ideas! 😃
There's another, more direct solution to the "running
include?
on anArray
takes longer and longer as the array gets bigger" problem: use aSet
.Add
require 'set'
to the top of your file; the library ships with Ruby but isn't loaded by default.Then replace
@@taken_names = []
with@@token_names = Set.new
.Under the hood,
Set
uses aHash
to get constant-time lookups - source. See your favorite Ruby internals book for details on this - I'm a fan of Ruby Under A Microscope.BTW, I was curious about the pitfalls of representing names as integers and then converting them - so I put together some code. Note: I've been writing a lot of Elixir lately, and the style reflects some of that.
This would make using the
Set
approach even more efficient, sinceInteger
objects are 8 bytes instead of 40 bytes forString
s.Pretty cool! I didn't know that about ranges :)
Be careful... since self.names is a class method, then this implementation of
@names
is also a class variable.So both of these are using class level variables and writing to the global namespace:
In this last case, the Robot class is a singleton instance itself.
Class variables are not 'evil' as long as you know what you are doing. Best rule of thumb would be to consider a class variable generally as immutable. In this example, if we had 2 separate processes sharing the same service of make robot names... one could reset the names while the other is running. This would cause a bad side effect. However, if you simple used the class variable to store the initial full set of names and then randomized it with each instance would be safer. Class variable is being used as a constant, immutable value... while random is per instance.
Also, since the built array already exists inside the enumerable, reusing the array is slightly faster than rebuilding from a range each time:
Not even a Ruby dev, but this was a good read. Interesting challenge and nifty solution :D
Thanks! Yeah, when I first read through the challenge, I didn’t think it was going to be that hard. I was wrong.
I think Ruby is fundamentally interesting, simple and fun. If it weren't for Rails it would still be known as more of a delightful hobbyist language.
Yeah, and a pretty powerful scripting/automation language too, for server-side stuff. A lot more readable and maintainable than Bash, a lot of times.
This is a fun challenge. I wanted to see if I could figure out a way to do this without shuffle. The solution I settled on after much googling is to use a linear congruential generator algorithm, which is a psuedorandom number generator with the property that, given the right inputs, its output is a sequence of all integers in a range (0 .. m-1) in pseudorandom order with no repeats (until it exhausts the range, whereupon it begins the sequence again). In other words, it can be used as a "lazy shuffle" algorithm. Here's the implementation I came up with, as a Ruby Enumerator:
The "increment" value
C
could have been any number coprime withM
; I chose77777
purely for aesthetics.Note that if we changed
M.times do
toloop do
, the sequence would cycle infinitely.Once you have this, all that's left is to format each number as a robot name, and then some bookkeeping for the
forget!
andreset!
methods. Here's the final result:Just for kicks, here's the same thing in JavaScript: beta.observablehq.com/@jrunning/ro... (Too bad dev.to doesn't embed Observable notebooks.)