Omitting named arguments values (Ruby 3.1 feature)

Ruby added this in 3.1:

https://rubyreferences.github.io/rubychanges//3.1.html#values-in-hash-literals-and-keyword-arguments-can-be-omitted

Here’s a small snippet:

x, y = 100, 200
# => [100, 200]
# In hash literal
h = {x:, y:}
# => {:x=>100, :y=>200}

# In method call
p(x:, y:)
# prints: "{:x=>100, :y=>200}"

In Crystal that would translate to named tuples and calls. Given that in Crystal you can use named arguments in every call, I think it could make code more succinct, specially when forwarding named arguments.

Searching for (\w+):\s(\1)\b in the compiler+std source code, there are 1671 matches, which is a lot!

Some random examples I found:

  def self.build(io : IO, version : String? = nil, encoding : String? = nil, indent = nil, quote_char = nil) : Nil
    build_fragment(io, indent: indent, quote_char: quote_char) do |xml|
      xml.start_document version, encoding
      yield xml
      # omit end_document because it is called in build_fragment
    end
  end

becomes:

  def self.build(io : IO, version : String? = nil, encoding : String? = nil, indent = nil, quote_char = nil) : Nil
    build_fragment(io, indent:, quote_char:) do |xml|
      xml.start_document version, encoding
      yield xml
      # omit end_document because it is called in build_fragment
    end
  end

Another one:

        request = HTTP::Request.from_io(
          input,
          max_request_line_size: max_request_line_size,
          max_headers_size: max_headers_size,
        )

becomes:

        request = HTTP::Request.from_io(
          input,
          max_request_line_size:,
          max_headers_size:,
        )

And this:

  def self.from_filetime(filetime) : ::Time
    seconds, nanoseconds = filetime_to_seconds_and_nanoseconds(filetime)
    ::Time.utc(seconds: seconds, nanoseconds: nanoseconds)
  end

becomes:

  def self.from_filetime(filetime) : ::Time
    seconds, nanoseconds = filetime_to_seconds_and_nanoseconds(filetime)
    ::Time.utc(seconds:, nanoseconds:)
  end

What are your thoughts about this? Should we add it?

6 Likes

Not a complete veto from my side but I have to say I’m not the biggest fan of this feature. It’s very implicit and IMO a language feature you need to know in order to understand the code. Yes this saves some verbose typing but I don’t feel like it’s actually removing that much noise when reading :)

15 Likes

If it’s simple enough to implement then I think it would make for a nice addition.
As a side effect it’ll probably encourage uniform variable naming, so might make codebases simpler to read

7 Likes

I wondered about porting this to Crystal when I saw it in Ruby’s release notes. But like @jhass I’m hestitant because the syntax is not very self-explanatory. I don’t think there’s a better alternative, otherwise Ruby would’ve probably chosen that one.

We surely don’t need to push this and could just give it some time to see how adoption in Ruby develops before adding it to Crystal…

8 Likes

It makes sense in Ruby because that’s the same syntax used for named parameters:

def foo(x:, y:)
  p(x:, y:)
end

x, y = 100, 200
foo(x:, y:) # => {:x=>100, :y=>200}

In Crystal, not so much. The closest approximation I could do is:

macro named(*args)
  NamedTuple.new(
    {% for arg in args %}
      {{ arg.id.stringify }}: {{ arg.id }},
    {% end %}
  )
end

def foo(*, x, y)
  p **named x, y
end

x, y = 100, 200
foo **named x, y # => {x: 100, y: 200}
2 Likes

I’m all in for this one

7 Likes

Yeah, there are some gotchas like ambiguity without parentheses in method calls and others. This is where JavaScript version makes more sense and looks clean, but the Ruby one is not.

1 Like

Not everybody is happy about it in the Ruby community either: Bad Ruby: Hash Value Omission - (think)

4 Likes

Am I missing something here.

Having it as a feature doesn’t force you to use it, right. If you write code with explicit variables it will still run, so I don’t see all the hostility over it. It’s like all the newer feature introduced in Ruby 3.0.

Old code will still run without breaking, and new code can be written like old code without breaking. So if you want to run new code on older versions, write it like old code. If you want to use new features, then you aren’t concerned with running it on older versions.

And that’s true of all languages as they add new features as they evolve.

Ruby added it because Matz accepted it. Period. It made him happy (or at least not sad).

If Crystal adds it, those who like it will use it, those who don’t won’t.

And life will go on.

Having it as a feature doesn’t force you to use it, right

Yes, it does. Eventually you’ll work with someone that uses it, or you’ll have to read library code that uses it. Reading that code means you need to understand it.

This is the thing one always has to have in mind when adding features: even if you don’t use it, there’s a big change you’ll need to know about it and understand it well.

15 Likes

Yes, of course. But you always have to understand whatever paradigms exist within a language to understand how a piece of code works, and why it was done like that.

I followed the original live discussions about this on the Ruby issues forum, and I saw all the arguments. But at the end of the day Matz accepted it, and that’s that.

You started this thread, so I assume you see some merit in considering it.
At the end of the day, the devs will decide. I don’t see how this is so hard to understand, whether you choose to use it or not.

This is exactly the point of having this discussion. To be productive in a given language you need know all its features, whether you choose to use them or not. Language design therefore is a question of whether the benefits of a feature outweigh the increased cognitive complexity required to fully understand the language. Primarily anyway, secondarily since a programming language suitable for non-trivial projects ultimately also is a language for communication between different people first and people and machines only second you also get all the social issues into this discussion too.

That’s why I’m hesisant to increasing the language complexity here, I don’t see the promised reduced cognitive load when reading programs using this feature, nor the promised reduced cognitive load when writing programs using this feature to outweigh the cognitive load of having to know and understand this feature in the first place. Secondarily as the blog post I linked outlines perfectly I also don’t see the benefits outweigh the general complexity increase it adds to the language from all the possible interactions with existing features.

5 Likes

Crystal has had great success in learning good things from Ruby. We should continue that and see how this Ruby feature develops. If it becomes a success, we can consider to copy it to Crystal.

8 Likes

I would call it “obfuscation”.
It makes code harder to understand. This feature will definitely come and bite people in the behind when they don’t expect it.

Not everything is awesome just because it exists in the ruby language.
Not every shortcut needs to be taken or offered.

6 Likes

For those that don’t know the feature, it certainly feels like that. But for those that knows it, it reduces the amount of code you have to read, which actually helps reading the code (more below).

That’s definitively true. As already mentioned by others, we can wait and see how it fairs in Ruby and then consider if we care about it.

Ultimately, this is one more point in the discussion of how easy we want the language to newcomers, and how productive we want it to experts. I’m certain that experts will incorporate such a feature quickly and find it useful, but it will always feel weird to newcomers.

I’m an example: OCaml is another language with a similar feature (syntax: ~name), and at first I was confused by it. When I got it, I find it useful and more readable than reading name: name, in particular when there are several arguments, and this is why I like to have it in Crystal too. This said, there is a big “but…”.

In every language experts tend to take the most important decisions, so I’m not surprised that Ruby (and Matz) decided in favor of it. If you have a large universe of experts out there, you can move the language forward: you have enough people to evangelize to care about what newcomers would feels. They will move onto experts quickly enough to worry about keeping the language simple.

Crystal doesn’t have that large universe of experts, so it might be wise to wait before complicating the syntax. Newcomers may be expelled simply because there aren’t that many experts to learn from.

4 Likes

I don’t think either of those things should happen because you have more or fewer experts.

Additions to the language should happen because they make the language come closer to its stated design principles.

New features should be avoided if it does not get the language closer, or run counter, to its design principle.

Thing is, i don’t even mind the feature itself. It might be a good idea to find a way to get rid of this duplication. just not in a way that suggests or (ab)uses allready existing syntax.

content hidden due to reckless use of weird characters :)

If we can find a way to say “use the local variable with the same name as the named parameter value if it exists” in a way that is easy to spot or even self explanatory i’d be all for it.
You could then even go as far as ommiting the names completely and just match all available local variables to the named parameters of the method.

fun( »use_local_variables« )
or
weird(¶name, ¶another_name)
or
actually_makes_some_sense( §name, §other_name)
or_like this( § )

2 Likes

Whatever(a_huge_name_of_something: a_huge_name_of_something)

Is hard to read, then it became

Whatever(a_huge_name_of_something:)

That passes the idea that the author was writing something, paused, and forgot to complete it

I would prefer to find an operator that represents “equals the value of the previous symbol” in certain contexts, like, let’s suppose:

Whatever(a_huge_name_of_something: ==)

A “==” alone in this context could mean it. It denotes the place of the value and the intention of the author.

Whatever(a_huge_name_of_something: ==, another_crazy_stuff: ==)

5 Likes

The problem is mixing feature with syntax. A lot of the complain in the “Bad Ruby” post mixed the two concepts. If we agree the feature is good, then the discussion should move to the syntax and constructively propose another one (as you did). That said, I still think that more syntax is more to be learned and a risk of making the language too complex too soon.

About the syntax, I like weird characters, and to me it’s a real shame inputs are not up to the task. If I have to do that symbol § (which I agree is quite good for this) I don’t even know how to find it. And once I know it’s name, I have to Ctrl-Cmd-Space then write part of its name (“section s”) and press two times enter. If I use it enough, then I can navigate with the arrows, yet it’s still unacceptable. This is the reason languages stick to keyboard-visible characters, and I think it’s a sane decision.

1 Like

Whatever(a_huge_name_of_something: ==)

I kinda like this idea the best, although not the use of ==.
The reason that I like this version of it more than “stuff(thing: )” is that it makes it obvious that a language feature is being used, it’s something that is googlable, it’s something that you can search the docs for. It’s very hard to search docs/google for the absence of something.

4 Likes