Bug #12521
closedSyntax for retrieving argument without removing it from double-splat catch-all
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 8 years ago
#12522 is a related issue; keyword arguments lvalues have a related issue.
Updated by matz (Yukihiro Matsumoto) over 8 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) over 8 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 nobu (Nobuyoshi Nakada) over 8 years ago
- Description updated (diff)
Updated by Eregon (Benoit Daloze) over 8 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) over 8 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.