Project

General

Profile

Bug #12521

Syntax for retrieving argument without removing it from double-splat catch-all

Added by gisborne (Guyren Howe) over 4 years ago. Updated about 4 years ago.

Status:
Closed
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:76154]

Description

There is an interesting style of programming that is almost really easy to do in Ruby. It would work elegantly with a simple change. A double-colon keyword argument should be available that will still leave the same argument captured by a double-splat argument if present. With this available, it becomes easy to pass down a "context" through a call chain.

Consider this:

def controller name::, **context
  
  log_then_render something:, name:, **context
end

def log_then_render **context
  log context
  complex_logic_then_render **context
end

def complex_logic_then_render name::, **context

Bunch of further calls

def render name::, something::, **context
  
end

Now assume I decide render needs a foo argument, that I obtain in my controller. The only functions that are aware of or have any need for the argument are controller and render. With functions written in this style, I only need to modify the two functions that need to know about the argument:

def controller name::, **context
  
  log_then_render something:, name:, foo: foo_value, **context
end

 no changes 

def render name::, something::, foo:: **context
   now use foo 
end

This is, I accept, unusual. I've not seen a language that offers this sort of feature (I call them, for various reason I don't have time to go into now, FREST functions). I can basically implement this now with a decorator, but it's a little ugly and slow.

It just occurred to me that an alternative would be a triple-splat final argument (or some such) that gathers all the keywords.

There is a related problem with the way double-splat and regular keyword arguments interact that should be fixed anyway.

Updated by gisborne (Guyren Howe) over 4 years ago

#12522 is a related issue; keyword arguments lvalues have a related issue.

Updated by matz (Yukihiro Matsumoto) about 4 years ago

  • Status changed from Open to Closed

I am not sure what we gain comparing to

def controller **context
  if context.key?(:name)
    ...
  end
end

Is this worth adding new syntax? If you think you can persuade me, please reopen.

Matz.

Updated by joker1007 (Tomohiro Hashidate) about 4 years ago

I want this feature.
Keyword argument is safer and more explanatory than splat style.
But I sometimes want to pass whole arguments to other methods.

Example.
CLI gem that uses AWS api and accepts api key, secret key, region, and endpoint as command options.

class KMS
  def initialize(key_id, region:, access_key_id: nil, secret_access_key: nil)
    @client = Aws::KMS::Client.new({
      region: region,
      access_key_id: access_key_id,
      secret_access_key: secret_access_key,
    })
    @key_id = key_id
  end

  def encrypt(value)
    resp = @client.encrypt(key_id: @key_id, plaintext: YAML.dump(value))
    Base64.strict_encode64(resp.ciphertext_blob)
  end

  def decrypt(value)
    resp = @client.decrypt(ciphertext_blob: Base64.strict_decode64(value))
    YAML.load(resp.plaintext)
  end
end

If I can use __keyargs__ (tentative name), I can omit redundant lines.

class KMS
  def initialize(key_id, region:, access_key_id: nil, secret_access_key: nil)
    @client = Aws::KMS::Client.new(__keyargs__) # simple
    @key_id = key_id
  end

  # ...

This example has few args, but I sometimes needs so many args when I kick complicated web API.

Updated by Eregon (Benoit Daloze) about 4 years ago

Tomohiro Hashidate wrote:

I want this feature.
Keyword argument is safer and more explanatory than splat style.
But I sometimes want to pass whole arguments to other methods.

Example.
CLI gem that uses AWS api and accepts api key, secret key, region, and endpoint as command options.

class KMS
  def initialize(key_id, region:, access_key_id: nil, secret_access_key: nil)
    @client = Aws::KMS::Client.new({
      region: region,
      access_key_id: access_key_id,
      secret_access_key: secret_access_key,
    })
    @key_id = key_id
  end
  # ...
end

If I can use __keyargs__ (tentative name), I can omit redundant lines.

class KMS
  def initialize(key_id, region:, access_key_id: nil, secret_access_key: nil)
    @client = Aws::KMS::Client.new(__keyargs__) # simple
    @key_id = key_id
  end

  # ...

Why is it better than this?

class KMS
  def initialize(key_id, **auth)
    @client = Aws::KMS::Client.new(**auth) # simple
    @key_id = key_id
  end

  # ...

It seems to me the Aws::KMS::Client should validate its arguments anyway, and this also reduces duplication.
If a different API must be provided for KMS#initialize, then I think the explicit style is much better as it's less magic and error-prone.

Updated by Eregon (Benoit Daloze) about 4 years ago

Guyren Howe wrote:

def render name::, something::, foo::, **context
   now use foo 
end

So this would be syntactic sugar for essentially this?

def render **context
  name = context.fetch :name
  something = context.fetch :something
  foo = context.fetch :foo
   now use foo 
end

It's true that's it's verbose.
I'm not particularly fond of double or triple operators with slightly different semantics though.

From a design point of view, it seems a parameter object would work well here,
and would potentially avoid conflicts for two :name with different meaning.

Also available in: Atom PDF