Project

General

Profile

Feature #11690

Update Hash during multiple assignment

Added by danielpclark (Daniel P. Clark) over 3 years ago. Updated over 3 years ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:71495]

Description

Given that we can assign multiple variables at once

a,b,c = 1,2,3

It would be nice to be able to update a Hash during multiple assignment rather than replacing it. Currently

x = {a: 1, b: 2}
x, y ,z = {c: 3}, 6, 7
x
# => {c: 3}

What I propose is adding Hash#update= to permit updating during multiple assignment.

class Hash
  def update=(h)
    update(h)
  end
end

x = {a: 1, b: 2}

x.update, y ,z = {c: 3}, 6, 7
x
# => {a: 1, b: 2, c: 3}

This would be most useful in scenarios where a method or proc return multiple values. When the method returns the values we don't normally know the key outside where the hash assignment is.

example = proc { [{:hi => :hello}, 5] }

hash = {}

# Currently in Ruby with an Unknown key multiple assignment isn't an option
hash[????], current = example.call

# We currently have to two step it
result, current = example.call
hash.update(result)

But with Hash#update= we don't have to know the key.

History

Updated by phluid61 (Matthew Kerwin) over 3 years ago

On 16/11/2015 2:32 AM, 6ftdan@gmail.com wrote:

This would be most useful in scenarios where a method or proc return
multiple values. When the method returns the values we don't normally know
the key outside where the hash assignment is.

example = proc { [{:hi => :hello}, 5] }

hash = {}

# Currently in Ruby with an Unknown key multiple assignment isn't an
 option
hash[????], current = example.call

# We currently have to two step it
result, current = example.call
hash.update(result)

But with Hash#update= we don't have to know the key.

I get the use-case, but I think the understandability of the code starts to
suffer.

What about something completely new but splat-like?

 hash[*], current = example.call

This is even better when the hash comes last, it looks more like an options
parameter:

 opts = get_default_hash
 a, b, opts[*] = example2.call

I would assume this also works in single assignment:

 opts = get_default_hash
 opts[*] = get_new_opts

Updated by danielpclark (Daniel P. Clark) over 3 years ago

I'm not sure the opts[*] is a good idea. You have have current splat behavior with multiple assignment.

a, *, c = [1,2,3,4,5]
a
# => 1
c
# => 5

And the opts[*] is unclear about what it is and what it expects. It could be a Proc.

prc = proc {|b,c,d| b+c+d }
# if [*] were implemented
a, prc[*], e = [1,2,3,4,5]

As far as I know Hash is the only core object with an update method. And in other cases where update is used, such as in Rails' ActiveRecord, a Hash is the expected input. So when the update method is called it is generally understood to be expecting a Hash object.

Implementing [*] would be much more work that simply implementing Hash#update=

I was thinking about suggesting Hash#merge!= as well but Ruby syntax does not allow the equals sign after and exclamation point. So I have chosen to only suggest Hash#update=

class Hash
  def update=(h)
    update(h)
  end
end

Updated by phluid61 (Matthew Kerwin) over 3 years ago

On 17/11/2015 12:18 AM, 6ftdan@gmail.com wrote:

Issue #11690 has been updated by Daniel P. Clark.

I'm not sure the opts[*] is a good idea. You have have current splat
behavior with multiple assignment.

a, *, c = [1,2,3,4,5]
a
# => 1
c
# => 5

And the opts[*] is unclear about what it is and what it expects. It
could be a Proc.

prc = proc {|b,c,d| b+c+d }
# if [*] were implemented
a, prc[*], c = [1,2,3,4,5]

Good point. I won't suggest anything to do with hash-splatting ** ;)

As far as I know Hash is the only core object with an update
method. And in other cases where update is used, such as in Rails'
ActiveRecord, a Hash is the expected input. So when the update method
is called it is generally understood to be expecting a Hash object.

Implementing [*] would be much more work that simply implementing
Hash#update=

I was thinking about suggesting Hash#merge!= as well but Ruby syntax
does not allow the equals sign after and exclamation point. So I have
chosen to only suggest Hash#update=

class Hash
  def update=(h)
    update(h)
  end
end

It's still very strange to think of 'update' as a property of a hash
object. Without some kind of new syntactic construct, with its own clear
semantics, this seems like a hack and doesn't feel right in the core (to
me; others may disagree.)

Updated by nobu (Nobuyoshi Nakada) over 3 years ago

Just an idea.

class ToBeAssigned
  def initialize(obj)
    @target = obj
  end
  def method_missing(m, *args)
    @target.__send__(m.to_s.chomp('='), *args)
  end
  def respond_to_missing?(m)
    @target.respond_to?(m.to_s.chomp('='))
  end
end
module Kernel
  def to_be_assigned_with
    ToBeAssigned.new(self)
  end
end

x = {a: 1, b: 2}

x.to_be_assigned_with.update, y ,z = {c: 3}, 6, 7
p x

Updated by danielpclark (Daniel P. Clark) over 3 years ago

Nobuyoshi Nakada wrote:

Just an idea.

x.to_be_assigned_with.update, y ,z = {c: 3}, 6, 7

I absolutely love the extensibility of this! In that it can be used on any object to allow multiple assignment with any method. Brilliant Nobuyoshi! Now if only the method name wasn't so long ;-)

Maybe we could replace to_be_assigned_with to slurp, consume, devour, or injest ;-) It properly expresses the additive nature of the action. Oooh! Or even feed_me. eg) x.feed_me.update ... Although I don't think most of those would make it in as an official method in Ruby; they're a bit silly.

Updated by nobu (Nobuyoshi Nakada) over 3 years ago

Daniel P. Clark wrote:

Maybe we could replace to_be_assigned_with to slurp, consume, devour, or injest ;-)

My dictionary doesn't have the last word, maybe a compound word from inject and ingest?

Updated by danielpclark (Daniel P. Clark) over 3 years ago

You're right! I misspelled it. If I had added a space "in jest" it would be "a joke". ingest is what I had meant to say. I wrote how it is pronounced rather than how it is spelled.

Updated by danielpclark (Daniel P. Clark) over 3 years ago

I was thinking of what word(s) may best clarify this and not conflict with other potential uses. One issue about the word assigned is it seems like self = or replace. I at first was thinking of to_be ... but that's sounds more like a promise of the future and each method would have to end in ed like so x.to_be.updated = a: 1. To avoid using a promise form and complex ed handling I was thinking of just to; x.to.update. But I don't think this is clear enough. So I have found the shortest thing I can think of that matches the statement to_be_assigned_with, and that is apply. It's short, simple, clear, and reveals to anyone that more is being done here.

To copy from Nobuyoshi's idea

class Applicable
  def initialize(obj)
    @target = obj
  end
  def method_missing(m, *args)
    @target.__send__(m.to_s.chomp('='), *args)
  end
  def respond_to_missing?(m)
    @target.respond_to?(m.to_s.chomp('='))
  end
end
module Kernel
  def apply
    Applicable.new(self)
  end
end

x = {a: 1, b: 2}

x.apply.update, y ,z = {c: 3}, 6, 7
p x

Arguably I think this is just as clear as to_be_assigned_with, and is different enough for people to notice that something else is going on here. That should be enough.

Updated by danielpclark (Daniel P. Clark) over 3 years ago

If we wanted to avoid chomping the equals sign on methods that have the equals already we'd have to write in a check for that. But I'm not sure that would be as performant.

class Applicable
  def initialize(obj)
    @target = obj
  end
  def method_missing(m, *args)
    m = @target.respond_to?(m) ? m : m.to_s.chomp('=')
    @target.__send__(m, *args)
  end
  def respond_to_missing?(m)
    @target.respond_to?(m.to_s.chomp('='))
  end
end
module Kernel
  def apply
    Applicable.new(self)
  end
end

x = {a: 1, b: 2}

x.apply.update, y ,z = {c: 3}, 6, 7

x
# => {:a=>1, :b=>2, :c=>3}

# Hash still responds to :[]= method now after :apply
x.apply[:z] = 0

x
# => {:a=>1, :b=>2, :c=>3, :z=>0}

I think the performance isn't an issue for this change. Anyone who uses the apply method should expect the added behaviors performance hit.

Updated by danielpclark (Daniel P. Clark) over 3 years ago

Even shorter Kernel#with

my_hash = {}
my_hash.with.update, b, c = ->{ {a: 1}, 5, 6 }.call

Updated by danielpclark (Daniel P. Clark) over 3 years ago

I made a gem for this. gem assign

I figure Kernel#assign is the best method name for this and the object it returns is an Assignable object.

Also available in: Atom PDF