-
-
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)
end
Thanks 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.