-
-
Save plexus/42c6c9c63212182ee440 to your computer and use it in GitHub Desktop.
| # How to update values on immutable objects? | |
| class Foo | |
| def initialize(attrs) | |
| @x = attrs.fetch(:x) | |
| @y = attrs.fetch(:y) | |
| freeze | |
| end | |
| end | |
| foo = Foo.new(x: 5, y: 7) | |
| # Option 0 | |
| # Use x=9. Would be great but doesn't work in Ruby, it will always | |
| # return it's argument so you can chain assignments | |
| # Option 1 | |
| class Foo | |
| attr_reader :x, :y | |
| def set_x(x) | |
| self.class.new(x: x, y: y) | |
| end | |
| end | |
| foo.set_x(9) # => #<Foo:0x007fbe1e7acc48 @x=9, @y=7> | |
| foo # => #<Foo:0x007fbe1e7acd60 @x=5, @y=7> | |
| # Option 2 | |
| Undefined = Object.new | |
| def Undefined.inspect ; 'Undefined' ; end | |
| class Foo | |
| def x(x = Undefined) | |
| if x == Undefined | |
| @x | |
| else | |
| self.class.new(x: x, y: @y) | |
| end | |
| end | |
| end | |
| foo.x(9) # => #<Foo:0x007fbe1e7ac4c8 @x=9, @y=7> | |
| foo # => #<Foo:0x007fbe1e7acd60 @x=5, @y=7> | |
| # Option 3 | |
| # Same as #1, but with_x, apparently common in Scala/Java | |
| # Option 4 | |
| # Anima style, use explicit "update" function | |
| require 'anima' | |
| class Foo | |
| include Anima.new(:x, :y) | |
| include Anima::Update | |
| end | |
| foo.update(x: 9) # => #<Foo x=9 y=7> | |
| foo # => #<Foo x=5 y=7> | |
| # Option 5 | |
| # ... | |
| # Any other ideas? |
+1 for option #2
Option 1
Option 2.
option 1.
Option 1 looks cleaner than option 2 BUT option 2 seems to be the right way to do it.
+1 for option 2!
Finally the hipster option
def โx(x)
self.class.new(x: x, y: @y)
endThanks a lot for the input! Some more considerations / tradeoffs
#1
Looks too much like it mutates the object
#2
I actually liked that best at first as well, but after using it extensively I'm coming back from it. It's happened to me a few times that I'm fetching an attribute instead of updating it, so somewhere later down the line a completely unexpected type of object pops up. It can also be very non-obvious when reading the code what it does. Finally the arity check is extra work, can give a small performance hit.
#3
I didn't know that Scala had this convention. It's different from set so more obvious it's not mutating, and already has precendent elsewhere. I think I'll start using this. Thanks @gamache and @eljojo for pointing it out.
#4
A great addition to #3 I think, has the benefit that you can update multiple attributes without allocating intermediate objects. It has been suggested to use with(x: 3) which would go really well with #3.
#3 and #4 :)
Option #4, but I would use copy to make it obvious that you get a new object back ๐
@plexus, reading your comments about it made me switch from #1 to #3 ๐
Option #4 is common in plain Scala where it's called copy. Probably for the reason @moonglum stated.
case class Foo(x: Int, y: Int)
val foo = Foo(5, 7) // => Foo(5, 7)
val bar = foo.copy(x = 9) // => Foo(9, 7)All those options will be a pain with nested, immutable data structures, though. At that point lenses will come in handy. I'm not aware of a library for that in Ruby, though. You can probably do something more convenient with Ruby meta magic anyway, though. I'd think something like this:
Person = Sruct.new :name, :address
Address = Struct.new :name, :number
person = Person.new "Hans", Addres.new("Doubledykes Rd.", 42)
person.copy(
name: "Richard",
address: {
number: 1
})As opposed to
person.copy(
name: "Richard",
address: person.address.copy(number: 1))Great to get so many replies on this! It seems some people also name their update / copy / with method new. I've seen this and think I might have actually used it at some point, although I'm not a fan because it's different enough from the class method new to cause confusion.
Option 2.