Shorthand hash syntax in Ruby

As the keyword discussion keeps going on and enflaming passions, there is a Ruby proposed feature I’ve been wanting to talk about for a long time: shorthand hash syntax.

You see, very often in code, I will take care to name my variable and methods well, and structure the whole thing clearly. The consequence of that structure is that I will very often end up with structures such as:[^2]

{
  topics: topics,
  users: users,
  very_longly_named_objects: very_longly_named_objects,
  even_more_very_longly_named_objects: even_more_very_longly_named_objects,
}

It makes a lot of sense when you think about it: the hash will often be a collection of named things, and if one name makes sense, it will often reflect the one you already used in your code.

However, it’s quite long and tedious to type.

It’s a thing I never noticed before Javascript introduced the feature of typing that same object as:

{topics, users, very_longly_named_objects}

In other words, when no value is given, it will convert into {variable_name: variable_name}

And now, whenever I’m typing in Ruby, the most beautiful programming language that exists*, I’m greenly jealous of Javascript.[^1]

It’s one of those patterns that now I know they could exist, I am bothered every time I’m not using them cause they’re so great. (It is one of two such patterns I know of :sweat_smile:)

It has been proposed in Ruby (core)#15236. Alas, it was closed as of July 2019.

I’d like to open the discussion up about it here, seeing there is a good base of Ruby users, and this platform lends well to community discussion.

In my opinion, this proposed syntax feature:

  • targets a pattern that is frequently used in a lot of Ruby code, Rails and non-Rails
  • saves a considerable amount of code when used
  • rewards good, descriptive variable/method naming
  • is concise, readable, clear, elegant, in harmony with the spirit of Ruby, namely beautiful code that optimizes for programmer happiness

That is why we should support the spec.


*: Source: myself.
[^1]: Ruby, being jealous of Javascript. Can you imagine?
[^2]: And please keep in mind that these are example names only, yes if that were reality there would be a way to better structure this code. This does not make the general idea moot.—Sadly this is a point some people raise in debate.

14 Likes

Without having read the core ruby issue and put much thought into it, it seems a tiny bit too magical to me.

I personally would prefer something slightly more explicit, maybe

{
  <<:topics, # (or topics:<< , or any other visual indication)
                  # (maybe even :topics: but that clashes a bit)
  users: User.all,
}

Having worked with Python and C as well, this syntax indicates a set or an enum to me, not a hash. A minor alteration to this - maybe something like {topics:, users:, very_longly_named_objects:} may be less ambiguous.

Definitely agree. I prefer Ruby to JavaScript in general, but JavaScript’s been making a lot of strides in the last few years and this shorthand syntax is one of the things I like.

I like it exactly as is in JavaScript, and it’s very Ruby-like in that it’s clean and minimalist.

Obviously if you know enough languages you’re constantly faced with syntax that means different things depending on language – eg. curly braces are used in C for arrays and enums, in Ruby for hashes, and in JavaScript for object literals.

But this syntax fits Ruby well.

I get why this makes sense but I find it comparatively ugly with the double punctuation:

{topics:, users:, very_longly_named_objects:}

It’s just so clean the other way.

2 Likes

One small reaction:

{topics:, users:, very_longly_named_objects:} feels to me as if the value for each was nil (nothing). But that’s a very subjective impression.

The point about this syntax representing a set has been raised before, what I would say is that I estimate that this ({var: var}) pattern is in more frequent usage than sets by a few orders of magnitude. Besides, as to semantics, in my opinion we’ve established that [] represented arrays and lists (to which set is similar, being a list of things), and {} represent keyed arrays and lists (or collections of pairs).

(Out of the blue, I could image having some sort of prefix for set/arrays, eg using a tilde as a shortcut to Array#to_set. So ~[:a, :b, :c] == [:a,:b,:c].to_set == Set[:a,:b,:c]. Just to demonstrate that we could find better literal syntaxes for sets.)

I think it’s quite feasible to find a shorthand hash syntax that is: unambiguous to the parser, concise, and ergonomic to the developers.

2 Likes

One of the better parts of the Javascript syntax we are jealous of is the ability to mix and match long and short forms as needed.

{
  topics,
  users,
  comments: current_user.comments
}
5 Likes

This. 10 times this.

Like many here, I too prefer Ruby over JavaScript, but I must confess this shorthand syntax is very nice, and I’m jealous of Js on this.

1 Like

I also think that feature might not be complete without dynamically named keys during initialization, which in JS looks like

{
  [variable]: 'value'
}

You can already “spread” (to speak in JS terms) in Ruby like

{ **another_hash }

similar to JS

{ ...other }

So that might be a consideration for deciding on syntax here as well.

Is there a reason you wouldn’t use { variable => 'value' } syntax? :wink:

1 Like

Nah, I still don’t buy the syntax. (For me) it lacks disambiguation between message sending and not message sending. Does { whatisthis, whatisthat } call something? Does it produce a hash with strings or with symbols as keys?

Besides, {} does not only denote a hash but can also define blocks/procs/lambdas. What is the “return” of { calculated_value }? Is it callable?

I agree, because in a method definition it would indicate a required named argument. Maybe the ‘{’,} itself could be used to clarify the intent. Note that probably I wouldnt really be convinced about it either, its brainstorming time:

:{
  key,
  other: othervalue,
  "stringkey": 42
}

Also when discussing the syntax maybe it makes more sense to include some code-context like in the ruby-issue.

  def notify source:, content
    Service::Message.queue_json :{source, content, type: Type::NORMAL}
  end
1 Like

I was thinking about this as well! I’d like to be able to write shorter syntax, especially for some really long key/value names.

I’d give my vote to:

{ topics:, users:, comments: current_user.comments }

Why?

It looks like keyword arguments and I’d expect it to fail if topics variable is undefined in the scope (or cannot be called if it’s a method).

To counter the argument “[it] feels to me as if the value for each was nil”, this is consistent with ruby keyword arguments syntax, and in that case it represents exactly the same thing like here: a required argument. If such argument is not passed, it will raise an error. (So nothing will be implicitly nil.)

The only difference from keyword arguments syntax is that, keyword arguments can be set to default values and overridden, while here they cannot:

def full_name(title: nil)
end

full_name # title is nil
title = 'Mr.'
arguments = { title: nil }
arguments[:title] # still nil

I don’t necessarily think it’s confusing to complicate the syntax though in the case of blocks (and especially procs and lambdas) since they’re contextual. i.e. you can’t

def test
  yield
end

test { 'key': 'value' }

but you can

test { { 'key': 'value' } }

So if the short hand was the same as javascript then in

def thing; end

test { thing }

thing sends a message and

thing = 1
test { { thing } }

is the same as

test { { thing: 1 } }

We see this disambiguation all over the place in JSX for passing dictionaries as variables, for example with inline styles

const style = { color: 'white' }
<div style={style} />

Is the same as

<div style={{color: 'white'}} />

It also brings to mind that ruby has a short hand for building a hash as the last param of a method call so

def test(p, h)
  puts p, h
end

test(1, key: 2, option: 3)

Outputs

1
{:key=>2, :option=>3)

The solution would would also need to be compatible with

key = 2
option = 3
test(1, key, option)

Which would be amazing.

2 Likes

How about using a % syntax? They seem to fit the bill quite well: a shorthand version of something that can already be defined in another way.

The “new” Ruby 1.9 hash syntax ({foo: 1}) was meant to replace the old one ({:foo => 1}).

On the contrary, %w(a b c) is not meant to replace ["a", "b", "c"]: it’s just shorter to write. Just like what you’re trying to achieve!

So how about something like %h(a b c) to generate {a: a, b: b, c: c}?

6 Likes

I really don’t hate this idea, it’s quite in line with the current syntax! I’d say it could be a good consensual first step.

However, how would we for instance mix shorthand values and explicit ones, à la {a, b, c: other::C} ?

— About block syntax, I’ve never really had this problem since I’ve used the convention hash is without inner spaces {a: 1, b: 2}, whereas inline blocks have them { [1,2] }. Not sure what this implies though.

Problem with that is you couldn’t do something like

%h(a b c d: something_else)

I use { a, b, c } in javascript a lot. It’s a useful shorthand, and I think it would be beneficial in ruby.

2 Likes

IMHO, shorthand notations are made for the most common cases, and it’s totally fine if they don’t handle all cases.

The fact that %h(a b c d) wouldn’t let you add {d: something_else} seems pretty much in line with the rest or Ruby to me: you can’t do ["a", "b", "c", 1] using the shorthand %w notation either.

Well, why not? This could go into the proposal. Its still brainstorming phase.

One more mention about the power of this in JavaScript (that I can think of right now) is that you can break up and assign “the rest” of the hash to a variable with spread. Most of the convo so far on short hand has revolved around “setting” but “getting” could use some love too

const hash = { a: 1, b: 2, c: 3 }
const { a, ...rest } = hash
console.log(a) # { a: 1}
console.log(rest) # { b: 2, c: 3 }

I could see a huge amount of controller cleanup being able to do something like

def create
  { email, **rest } = create_params
  user = User.find_by_email email
  # todo: check if that user exists or invite them dumby!

  # notice short hand for ThingUser.new(thing: @thing, user: user, title: create_params[:title], role: create_params[:role])
  @thing_user = ThingUser.new(thing: @thing, user, **rest)
  if @thing_user.save
    ...etc.
  end
end

def create_params
  params.require(:thing_user).permit(:email, :title, :role)
end
1 Like

I also find javascript’s shorthand for hashes elegant, handy and easy to read.

Both the hash (AKA object) literal…

config = {color, size, angle}

and the accessor shorthand…

{color, size, angle} = config

I also appreciate the spread operator, which is handy for merging hashes and arrays.

options = {...config, color: 'new color' }

or

options = {...config, ...settings}

I also agree with the sentiment that Javascript is a pretty ugly language on the whole but these shortcuts represent a rare moment of javascript elegance.

I don’t think Ruby should copy any other language slavishly but it’s interesting to think about which other languages Ruby developers are most likely to be familiar with. In 2020, I expect that’s overwhelmingly Javascript.

3 Likes

Somewhat related is the idea (and syntax) of anonymous structs (Anonymous Struct Literals Might Be Coming To Ruby | Super Good Software - I think I read that the PR is merged?).

1 Like