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