For example, at a previous company, we found the minimum of a set of heuristics (implemented as arrays) for a matchmaking algorithm
A single min_of will only work for 2-dimensional arrays — deeper nesting requires recursive calls
If there were a brand-new method for it and it called itself recursively (like dig does), it might be pretty interesting, but it would probably be worth understanding the use cases a bit more in case it’s not broadly useful.
That last line of code is equivalent to array.min_of(&.min). When you are simply calling a method on the value passed to the block, you can omit the explicit block and its block variables and replace it with &.method_to_call.
So, for example, these two are the exact same:
foo { |x| x.bar }
foo(&.bar)
It comes from a similar concept in Ruby (originally introduced in Rails), where the & method was defined on instances of Symbol to do the same thing:
foo(&:bar)
Crystal’s implementation is more powerful, though, since it allows passing arguments to the called method and even allows method chaining. Ruby’s implementation only allows it for the most trivial call: single method called on the block argument, no arguments, no chaining.
Assuming I’ve done my coding correctly (if you’re not careful, the compiler will optimize the code too much in a benchmark block and you’ll get invalid results), your best option varies:
If you know the number of dimensions of your data already, the arr.min_of solution, used to the appropriate depth, will be the absolute fastest and will use no new memory.
If you don’t know the number of dimensions of your data…
And you care much more about speed than memory usage, the arr.flatten.min solution will probably be the one you want.
And you’re fairly concerned about memory usage, the arr.each.flatten.min solution ought to work well
Notes on the code and output:
I used 1D, 2D, and 3D arrays each totaling 1 million elements for the benchmarks; the array copy method might be worse as you increase the total number of elements, but I don’t know.
The benchmark columns are
name average_iterations_per_second (average_runtime) (percentage_variation_across_runs) memory_usage comparison_to_fastest
Nice benchmark write-up. It’s also interesting that the ‘B/opp’ for ‘recursive_min_iterator’ is between the two but increases as the dimensions increase… and of course will vary depending on specific use case.
I’ve found an ideal solution for arrays in an unknown number of dimensions! It’s as fast as the #min/#min_of solution for each dimension I benchmarked and allocates no new memory. The only downside is that there’s more code:
def paired_recursive_min(arr : Array(Array))
arr.min_of { |internal_arr| paired_recursive_min internal_arr }
end
def paired_recursive_min(arr : Array)
arr.min
end
It’s pretty simple to read, though. It’s essentially the same as a recursive method where we check each time whether our array contains arrays, except that by making it two methods the type system handles it for us.
Here are my new benchmarks (the rest of the code is the same, just with the new methods defined and added to the benchmarks):