ES6 style object destructuring in Ruby
Check out the JavaScript ES6 object destructuring documentation for more information.
This was primarily a learning exercise to understand how this newer ES6 feature could work under the hood. We're not currently using this in production anywhere but it was a pretty fun challenge to solve.
Ruby 2.3+ already has some built-in methods and operators for simple object destructuring:
Array#dig
Hash#dig
Struct#dig
Array#values_at
Hash#values_at
- Splat operator
*
- Safe navigation operator
&.
This gem introduces a couple of new methods to the Object
class for more complex destructuring.
Object#dig
Object#destruct
It's mostly useful for fetching multiple nested values out of objects in a single method call.
Add this gem to the project Gemfile
.
gem "destruct"
This behaves just like the dig
methods in Array
, Hash
, and Struct
allowing ALL objects to be destructured.
The implementation simply uses send
to pass valid method calls thru to objects recursively.
class Object
def dig(method, *paths)
object = send(method) if respond_to?(method)
paths.any? ? object&.dig(*paths) : object
end
end
This method behaves very similar to the safe navigation operator &.
but checks if the object responds to the method before attempting to call it. Invalid method calls return nil
instead of raising NoMethodError
.
"test".dig(:upcase, :reverse) # "TSET"
"test".dig(:invalid, :chain, :of, :methods) # nil
It also delegates to native dig
implementations for Array
, Hash
, or Struct
objects whenever possible.
class Blog
def posts
[
{ "title" => "Testing" },
{ "title" => "Example" }
]
end
end
Blog.new.dig(:posts, 1, "title") # "Example"
This method is like a hybrid of all the other native Ruby destructuring methods! Let's define an example object:
object = {
id: 123,
title: "Hi",
translations: [
{
locale: "es_MX",
last_edit: "2014-04-14T08:43:37",
title: "Hola"
}
],
url: "/hi-123"
}
It behaves like values_at
and looks up values by keys:
id, url = object.destruct(:id, :url)
puts id # 123
puts url # "/hi-123"
It behaves like dig
to lookup nested values:
title, locale_title = object.destruct(:title, [:translations, 0, :title])
puts title # "Hi"
puts locale_title # "Hola"
It accepts hashes to dig
out nested values as well:
locale, title = object.destruct(translations: { 0 => [:locale, :title] })
puts locale # "es_MX"
puts title # "Hola"
It accepts a mixture of different argument types:
title, last_edit, locale, locale_title = object.destruct(
:title,
[:translations, 0, :last_edit],
translations: { 0 => [:locale, :title] }
)
puts title # "Hi"
puts last_edit # "2014-04-14T08:43:37"
puts locale # "es_MX"
puts locale_title # "Hola"
It accepts a block to lookup nested values with a clear and convenient DSL:
title, last_edit, locale, url = object.destruct do
title
translations[0].last_edit
translations[0][:locale]
url
end
puts title # "Hi"
puts last_edit # "2014-04-14T08:43:37"
puts locale # "es_MX"
puts url # "/hi-123"
It returns a Destruct::Hash
object when the return values are not splatted:
destructured = object.destruct do
title
translations[0].last_edit
translations[0][:locale]
url
end
puts destructured.title # "Hi"
puts destructured[:title] # "Hi"
puts destructured[0] # "Hi"
puts destructured.last_edit # "2014-04-14T08:43:37"
puts destructured.locale # "es_MX"
puts destructured.url # "/hi-123"
puts destructured[-1] # "/hi-123"
puts destructured[999] # nil
puts destructured[:missing] # nil
puts destructured.missing # NoMethodError
Note that Destruct::Hash
values are overwritten if there are multiple with the same keys:
destructured = object.destruct(:title, [:translations, 0, :title])
puts destructured.title # "Hola"
# This is where the index lookups really come in handy
puts destructured[0] # "Hi"
puts destructured[1] # "Hola"
The return value destructuring is done using Destruct::Hash#to_ary
for implicit Array
conversion!
Let's compare some of the JavaScript ES6 destructuring examples with their Ruby equivalents.
Note that almost all of these examples simply use native Ruby 2.3+ features!
var foo = ["one", "two", "three"];
var [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
foo = ["one", "two", "three"]
one, two, three = foo
puts one # "one"
puts two # "two"
puts three # "three"
var [a=5, b=7] = [1];
console.log(a); // 1
console.log(b); // 7
a, b = [1]
a ||= 5
b ||= 7
puts a # 1
puts b # 7
var a = 1;
var b = 3;
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1
a = 1
b = 3
a, b = b, a
puts a # 3
puts b # 1
function f() {
return [1, 2];
}
var [a, b] = f();
console.log(a); // 1
console.log(b); // 2
def f
[1, 2]
end
a, b = f
puts a # 1
puts b # 2
function f() {
return [1, 2, 3];
}
var [a, , b] = f();
console.log(a); // 1
console.log(b); // 3
def f
[1, 2, 3]
end
a, _, b = f
puts a # 1
puts b # 3
var [a, b] = [1, 2, 3, 4];
console.log(a); // 1
console.log(b); // 2
a, b = [1, 2, 3, 4]
puts a # 1
puts b # 2
var [a, b, ...c] = [1, 2, 3, 4];
console.log(c); // [3, 4]
a, b, *c = [1, 2, 3, 4]
puts c.inspect # [3, 4]
const avengers = [
"Natasha Romanoff",
["Tony Stark", "James Rhodes"],
["Steve Rogers", "Sam Wilson"]
];
const [blackWidow, [ironMan, warMachine], [cap, falcon]] = avengers;
console.log(warMachine); // "James Rhodes"
avengers = [
"Natasha Romanoff",
["Tony Stark", "James Rhodes"],
["Steve Rogers", "Sam Wilson"]
]
black_widow, iron_man, war_machine, cap, falcon = avengers.flatten
puts war_machine # "James Rhodes"
const avengers = [
"Natasha Romanoff",
[["Tony Stark", "Pepper Potts"], "James Rhodes"],
["Steve Rogers", "Sam Wilson"]
];
const [, [[, potts ]]] = avengers;
console.log(potts); // "Pepper Potts"
avengers = [
"Natasha Romanoff",
[["Tony Stark", "Pepper Potts"], "James Rhodes"],
["Steve Rogers", "Sam Wilson"]
]
potts = avengers.dig(1, 0, 1)
puts potts # "Pepper Potts"
var url = "https://developer.mozilla.org/en-US/Web/JavaScript";
var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
console.log(parsedURL); // ["https://developer.mozilla.org/en-US/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]
var [, protocol, fullhost, fullpath] = parsedURL;
console.log(protocol); // "https"
url = "https://developer.mozilla.org/en-US/Web/JavaScript"
parsed_url = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.match(url).to_a
puts parsed_url.inspect # ["https://developer.mozilla.org/en-US/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]
_, protocol, fullhost, fullpath = parsed_url.to_a
puts protocol # "https"
var o = {p: 42, q: true};
var {p, q} = o;
console.log(p); // 42
console.log(q); // true
o = { p: 42, q: true }
p, q = o.values_at(:p, :q)
puts p # 42
puts q # true
var o = {p: 42, q: true};
var {p: foo, q: bar} = o;
console.log(foo); // 42
console.log(bar); // true
o = { p: 42, q: true }
foo, bar = o.values_at(:p, :q)
puts foo # 42
puts bar # true
var {a=10, b=5} = {a: 3};
console.log(a); // 3
console.log(b); // 5
a, b = { a: 3 }.values_at(:a, :b)
a ||= 10
b ||= 5
puts a # 3
puts b # 5
function drawES6Chart({size = "big", cords = { x: 0, y: 0 }, radius = 25} = {}) {
console.log(size, cords, radius);
// do some chart drawing
}
drawES6Chart({
cords: { x: 18, y: 30 },
radius: 30
});
def draw_es6_chart(size: "big", cords: { x: 0, y: 0 }, radius: 25)
puts size, cords, radius
# do some chart drawing
end
draw_es6_chart(
cords: { x: 18, y: 30 },
radius: 30
)
var metadata = {
title: "Scratchpad",
translations: [
{
locale: "de",
localization_tags: [ ],
last_edit: "2014-04-14T08:43:37",
url: "/de/docs/Tools/Scratchpad",
title: "JavaScript-Umgebung"
}
],
url: "/en-US/docs/Tools/Scratchpad"
};
var { title: englishTitle, translations: [{ title: localeTitle }] } = metadata;
console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"
metadata = {
title: "Scratchpad",
translations: [
{
locale: "de",
localization_tags: [ ],
last_edit: "2014-04-14T08:43:37",
url: "/de/docs/Tools/Scratchpad",
title: "JavaScript-Umgebung"
}
],
url: "/en-US/docs/Tools/Scratchpad"
}
english_title, locale_title = metadata.destruct do
title
translations[0].title
end
puts english_title # "Scratchpad"
puts locale_title # "JavaScript-Umgebung"
var people = [
{
name: "Mike Smith",
family: {
mother: "Jane Smith",
father: "Harry Smith",
sister: "Samantha Smith"
},
age: 35
},
{
name: "Tom Jones",
family: {
mother: "Norah Jones",
father: "Richard Jones",
brother: "Howard Jones"
},
age: 25
}
];
for (var {name: n, family: { father: f } } of people) {
console.log("Name: " + n + ", Father: " + f);
}
// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"
people = [
{
name: "Mike Smith",
family: {
mother: "Jane Smith",
father: "Harry Smith",
sister: "Samantha Smith"
},
age: 35
},
{
name: "Tom Jones",
family: {
mother: "Norah Jones",
father: "Richard Jones",
brother: "Howard Jones"
},
age: 25
}
]
people.each do |person|
n, f = person.destruct(:name, family: :father)
puts "Name: #{n}, Father: #{f}"
end
# "Name: Mike Smith, Father: Harry Smith"
# "Name: Tom Jones, Father: Richard Jones"
function userId({id}) {
return id;
}
function whois({displayName: displayName, fullName: {firstName: name}}){
console.log(displayName + " is " + name);
}
var user = {
id: 42,
displayName: "jdoe",
fullName: {
firstName: "John",
lastName: "Doe"
}
};
console.log("userId: " + userId(user)); // "userId: 42"
whois(user); // "jdoe is John"
def user_id(id:)
id
end
def whois(display_name:, full_name:)
puts "#{display_name} is #{full_name[:first_name]}"
end
user = {
id: 42,
displayName: "jdoe",
fullName: {
firstName: "John",
lastName: "Doe"
}
}
puts "userId: #{user_id(user)}" # "userId: 42"
whois(user) # "jdoe is John"
let key = "z";
let { [key]: foo } = { z: "bar" };
console.log(foo); // "bar"
key = :z
foo = { z: "bar" }[key]
puts foo # "bar"
bundle exec rspec
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so we don't break it in a future version unintentionally.
- Commit, do not mess with the version or history.
- Open a pull request. Bonus points for topic branches.
MIT - Copyright © 2016 LendingHome