Comparing Ruby's Splat with JavaScript's Spread

At work, we have been developing a mentorship and training program to help provide our team members time for deliberate practice. A recent Ruby discussion led to reviewing the Ruby "splat" operator and comparing it to spread in JavaScript.

In this post, I'll cover the different ways these operators can be used in both Ruby and JavaScript.

Arrays

Building up Array

In both Ruby and JavaScript, you can use splat/spread to build up a new array from existing arrays.

Ruby

# create initial array
> arr1 = [1, 2, 3]
=> [1, 2, 3]

# without the splat operator
> [arr1, 4]
=> [[1, 2, 3], 4]

# with the splat operator
> [*arr1, 4]
=> [1, 2, 3, 4]

# combining multiple array
[*[1, 2, 3], 4, 5, *[6, 7, 8]]
=> [1, 2, 3, 4, 5, 6, 7, 8]

Idiomatic Ruby

In Ruby, this approach does not seem very common. Instead, it seems more common to use the + operator:

# create initial arrays
> a1 = [1, 2, 3]
=> [1, 2, 3]
> a2 = [3, 4, 5]
=> [3, 4, 5]

# create a new, combined array
a1 + a2
=> [1, 2, 3, 3, 4, 5]

It's probably even more common to see options that update (or mutate) the original array rather than returning a new one, but that is a different use case than what we are covering here.

JavaScript

I see this more commonly used in JavaScript, especially in projects that follow functional paradigms. Because this builds up a new array, you are not mutating state. This means you can use this strategy and still define pure functions.

// create initial array
> let arr1 = [1, 2, 3]
=> (3) [1, 2, 3]

// without the spread operator
> [arr1, 4]
=> (2) [Array(3), 4]

// with the spread operator
> [...arr1, 4]
(4) [1, 2, 3, 4]

// combining multiple array
> [...[1, 2, 3], 4, 5, ...[6, 7, 8]]
=> (8) [1, 2, 3, 4, 5, 6, 7, 8]

Passing into functions

These operators allow functions to take in an unknown number of arguments and will combine them into a single array argument.

Ruby

This is how I most commonly see this operator used in Ruby-land. I think this is because it can lend itself well to building DSLs that look more like written prose.

def greet_friends(*friends)
  "Hello, #{friends.join(', ')}"
end

> greet_friends "joey"
=> "Hello, joey"

> greet_friends "joey", "ross", "rachel", "chandler", "phoebe", "monica"
=> "Hello, joey, ross, rachel, chandler, phoebe, monica"

JavaScript

const greetClassmates = (...classmates) => (
  `Hello, ${classmates.join(", ")}` 
)

> greetClassmates("abed", "annie", "troy", "jeff", "britta", "pierce", "shirley", "señor chang")
=> "Hello, abed, annie, troy, jeff, britta, pierce, shirley, señor chang"

You may also see this referred to as the rest parameter syntax.

Hashes and Objects

For our Ruby examples, we will be using a Hash. For our JavaScript examples, we will be using an Object. Fortunately, the syntax for creating the structures is the same in both languages.

Two options for Ruby

In Ruby, you can use the Array version of the splat operator (*) for some operations, but there is also a Hash-specific operator as well (**). Here's how they both work:

# create our initial hash
> h = { name: "morty smith", age: 14 }

# use `*` operator, or array-version of splat
> ar = *h
=> [[:name, "morty smith"], [:age, 14]]

# turn this back into a Hash with 
# https://www.rubydoc.info/stdlib/core/Kernel:Hash
> Hash[[[:name, "morty smith"], [:age, 14]]]
=> {:name=>"morty smith", :age=>14}

# use the `**` operator, or hash-version of splat
> h2 = {**h}
=> {:name=>"morty smith", :age=>14}

We will go through the ** operator as it behaves more similarly to the spread operator in JavaScript. It's worth noting the behavior of the * version as it's similar to what you may encounter when working with the Enumberable module.

> h.map { |element| element }
=> [[:name, "morty smith"], [:age, 14]]

Building up new objects

Just like we saw above, we can use these operators to build up new hashes/objects from existing ones.

Ruby

# create our initial hash
> bob = {name: "Bob", age: 46}

# without the splat operator,
# you will get a syntax error
> {bob, business: "Bob's Burgers"}
=> SyntaxError ((irb):3: syntax error, unexpected ',', expecting =>)

# splat it to create a new hash with additional fields
> {**bob, business: "Bob's Burgers"}
=> {:name=>"Bob", :age=>46, :business=>"Bob's Burgers"}

JavaScript

// create our initial object
> let richard = {name: "Richard" , age: 31}

// without the spread operator,
// javascript will re-use the variable name as the key
// with a value of the original object
> {richard, business: "Pied Piper"}
=> {richard:  {name: "Richard" , age: 31}, business: "Pied Piper"}

// spread it to create a new object
// with additional fields
> { ...richard, business: "Pied Piper"}
=> {name: "Richard", age: 31, business: "Pied Piper"}

Passing into functions

Ruby

def greet(name, **options)
  greeting = options[:greeting] || "Hello"

  "#{greeting}, #{name}"
end

greet("teddy")
=> "Hello, teddy"

greet("mort", greeting: "Hiya")
=> "Hiya, mort"

In Ruby, you can leave off the ** and it will default to allowing your last argument to be a hash (or not):

def greet_two(name, options={})
  greeting = options[:greeting] || "Hello"

  "#{greeting}, #{name}"
end

greet_two("jimmy", greeting: "get out of here")
=> "get out of here, jimmy" 

greet_two("johnny")
=> "Hello, johnny" 

you can also treat the last argument as not a Hash

def greet_three(name, greeting)
  "#{greeting}, #{name}"
end

# pass in not a hash
greet_three(
  "Mr. Fischoeder",
  "We'll have the rent next week"
)
=> "We'll have the rent next week, Mr. Fischoeder"

# it can accept a hash,
# but we didn't set it up to handle that very well
> greet_three(
  "Mr. Fischoeder",
  greeting: "We'll have the rent next week"
)
=> "{:greeting=>\"We'll have the rent next week\"}, Mr. Fischoeder"

JavaScript

JavaScript doesn't support the curly-bracket-less key-value pair syntax as a function argument.

const f = (...o) => { console.log(o) }

> f(a: 1, b: 2)
=> VM202:1 Uncaught SyntaxError: missing ) after argument list

Instead, you can combine JavaScript's support for object destructuring with the splat operator. This lets you pull out certain parts of the object and keep a reference to everything else.

const fullCast = {
  stars: ["Leading Lady", "Main Man"],
  supporting: ["Nosy Neighbor", "Funny Friend"]
}

// Destructure the object to access `stars`,
// but don't get access anything else
const basicCredits = ({stars: stars}) => {
  console.log(`Starring: ${stars.join(", ")}!`)
}

> basicCredits(fullCast)
=> "Starring: Leading Lady, Main Man!"

// Destructure the object to access `stars`,
// and put the rest of the object in `rest`
const inclusiveCredits = ({stars: stars, ...rest}) => {
  console.log(`Starring: ${stars.join(", ")}!`)
  console.log(`Full cast includes...`, rest)
}

> inclusiveCredits(fullCast)
=> "Starring: Leading Lady, Main Man!"
   "Full cast includes... {supporting: Array(2)}"

Conclusion

We've covered how the splat and spread operators can be used in their respective languages for building up new instances of data structures as well as for function arguments.

Hopefully, this post spread some knowledge, and can be useful as a quick reference for how to use these operators in different scenarios.


Notice something wrong? Please consider proposing an edit or opening an issue.