Combining hashes with Hash#merge
There are many situations where it can be useful to combine hashes together. For example, to apply default values to a hash of options, or to combine two sets of configuration together. Ruby provides the Hash#merge(other_hash) method for this purpose, which returns a new hash containing the contents of both hashes, with the right-hand side hash taking precedence in case of key conflicts:
require "json"
require "net/http"
class JsonHttpClient
DEFAULT_HEADERS = {
"Content-Type" => "application/json",
"Accept" => "application/json"
}.freeze
def initialize(tenant_id, api_key)
@tenant_id = tenant_id
@api_key = api_key
end
def post(url, params = {})
headers = DEFAULT_HEADERS.merge(
"Authorization" => "Bearer #{@api_key}"
)
tenant_scoped_params = params.merge(tenant_id: @tenant_id)
Net::HTTP.post(URI(url), tenant_scoped_params.to_json, headers)
end
end
client = JsonHttpClient.new("tenant_123", "secret_api_key")
client.post("https://api.example.com/people", { name: "David" })
class JsonHttpClient {
static DEFAULT_HEADERS = {
"Content-Type": "application/json",
Accept: "application/json",
};
constructor(tenantId, apiKey) {
this.tenantId = tenantId;
this.apiKey = apiKey;
}
post(url, params = {}) {
const headers = {
...JsonHttpClient.DEFAULT_HEADERS,
Authorization: `Bearer ${this.apiKey}`,
};
const tenantScopedParams = {
...params,
tenant_id: this.tenantId,
};
return fetch(url, {
method: "POST",
headers,
body: JSON.stringify(tenantScopedParams),
});
}
}
const client = new JsonHttpClient("tenant_123", "secret_api_key");
client.post("https://api.example.com/people", { name: "David" });
In case we need to resolve conflicts in a more complex way than "right-hand side wins", merge also accepts a block that will be called for each conflicting key, with the key and both values passed in as arguments. The block should return the value to use for that key in the resulting hash:
base_config = {
timeout: 5,
retries: 2,
tags: ["base"]
}
slow_request_config = {
timeout: 60,
tags: ["slow"]
}
base_config.merge(slow_request_config) do |_key, old_value, new_value|
if old_value.is_a?(Array) && new_value.is_a?(Array)
old_value + new_value
else
new_value
end
end
# => { timeout: 60, retries: 2, tags: ["base", "slow"] }
const baseConfig = {
timeout: 5,
retries: 2,
tags: ["base"],
};
const slowRequestConfig = {
timeout: 60,
tags: ["slow"],
};
const merged = Object.entries(slowRequestConfig).reduce(
(acc, [key, value]) => ({
...acc,
[key]:
Array.isArray(acc[key]) && Array.isArray(value)
? [...acc[key], ...value]
: value,
}),
{ ...baseConfig }
);
// => { timeout: 60, retries: 2, tags: ["base", "slow"] }
As with many similar methods in Ruby, there is also a bang version merge! that mutates the original hash instead of returning a new one. This is useful when dealing with large hashes where performance is a concern, or when we explicitly want to mutate the original hash for some reason.
History
Hash#merge was added in Ruby 1.8, released in 2003. However, before that, Ruby had a Hash#update method that provided similar functionality for merging hashes in place (mutating the original hash). The original implementation of merge essentially just duplicated the hash and then called update on it.
In Perl, it is commonplace to merge hashes using the %merged = (%hash1, %hash2) syntax, which may have inspired Ruby's merge functionality, though there is no obvious single method in Perl that corresponds directly to Hash#merge.