A bit of Groovy / Ruby comparison today! Recently I needed to write a Groovy script that needed to convert an array of objects to a structured and nested report. And I stumbled on Groovy’s default values for Hashes, described here. I loved how you can create nested hashes with default on all levels:
nested_hash = [:].withDefault() { [:].withDefault() {0} }
It felt so nice to remove all those checks (if a key exists … otherwise …) and it cleaned up the code nicely, making it more intent revealing. A big plus!
So, I wanted to see how Ruby would do. And gues what? It didn’t feel as natural at first sight.
For example, I expected this to work out of the box:
nested_hash = Hash.new(Hash.new(0))
But, very fishy things started to happen, making feel lost quite a bit. A correct description of issues with this approach is summarised in this Stackoverflow question.
I guess this comes from the fact that Groovy treats non-existent hash key access with default value differently:
- Groovy creates non-existent keys by default
- Ruby doesn’t, it just returns the default value
The effect is such that with Groovy the default value is never changed, and you pile up items in your hash by accessing non-existent items.
For Ruby, a bit different approach is needed:
nested_hash = Hash.new { |hash, key| hash[key] = Hash.new(0) }
In effect, this makes Ruby nested hash behave as Groovy one: creating non-existent keys when accessed. At the above link, you can find a solution to have endlessly deep hash if you’re interested.
Finally, I decided to solve a small scenario in both languages. The idea was that while having a family, one needs to create a report stating age group / decade distribution by gender for it. Nested hashes / dictionaries with default values seemed ideal for it. Here are implementations in both languages (in no particular order):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Person { | |
public name | |
public gender | |
public age | |
public Person(name, gender, age) { | |
this.name = name | |
this.gender = gender | |
this.age = age | |
} | |
def decade() { | |
return "${age – (age % 10)}+" | |
} | |
} | |
family = [ | |
new Person("Sofia", "female", 1), | |
new Person("Aiko", "female", 1), | |
new Person("Vanja", "male", 37), | |
new Person("Branka", "female", 38), | |
new Person("Fuma", "female", 83), | |
new Person("Ranko", "male", 64), | |
new Person("Mira", "female", 64), | |
new Person("Tomo", "male", 67), | |
new Person("Zorica", "female", 66), | |
new Person("Greta", "female", 14), | |
new Person("Gael", "male", 21), | |
new Person("Vigo", "male", 11), | |
new Person("Mitchell", "male", 43), | |
new Person("Michel", "female", 22) | |
] | |
by_gender_and_decade = [:].withDefault() { [:].withDefault() {0} } | |
family.each { person -> | |
by_gender_and_decade[person.gender][person.decade()] += 1 | |
} | |
def prettify(hash) { | |
return hash.collect { key, value -> "${key} / ${value}" }.join(", ") | |
} | |
println "Family members by gender and age group:" | |
println "female: ${prettify(by_gender_and_decade["female"])}" | |
println "male: ${prettify(by_gender_and_decade["male"])}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Person = Struct.new(:name, :gender, :age) { | |
def decade | |
"#{age – (age % 10)}+" | |
end | |
} | |
family = [ | |
Person.new("Sofia", :female, 1), | |
Person.new("Aiko", :female, 1), | |
Person.new("Vanja", :male, 37), | |
Person.new("Branka", :female, 38), | |
Person.new("Fuma", :female, 83), | |
Person.new("Ranko", :male, 64), | |
Person.new("Mira", :female, 64), | |
Person.new("Tomo", :male, 67), | |
Person.new("Zorica", :female, 66), | |
Person.new("Greta", :female, 14), | |
Person.new("Gael", :male, 21), | |
Person.new("Vigo", :male, 11), | |
Person.new("Mitchell", :male, 43), | |
Person.new("Michel", :female, 22) | |
] | |
by_gender_and_decade = Hash.new { |hash, key| hash[key] = Hash.new(0) } | |
family.each do |person| | |
by_gender_and_decade[person.gender][person.decade] += 1 | |
end | |
def prettify(hash) | |
hash.collect { |key, value| "#{key} / #{value}" }.join(", ") | |
end | |
puts "Family members by gender and age group:" | |
puts "female: #{prettify(by_gender_and_decade[:female])}" | |
puts "male: #{prettify(by_gender_and_decade[:male])}" |
I like how Ruby is less verbose, especially regarding Person class / struct. But I must admit I like Groovy default nested hash values more, it somehow feels more natural. Then again, maybe for this situation the automatic creation of non-existent keys is good, but somewhere else it might not be such a good idea, so having the option to choose is worthwhile. I guess one just needs to get used to the flavor of the language used and that’s it. The rest of the code is pretty much the same, no surprises there. And in case you were wondering, yes, some of the names are from my own family 🙂