Skip to content

Instantly share code, notes, and snippets.

@plexus
Created December 9, 2014 14:01
Show Gist options
  • Save plexus/42c6c9c63212182ee440 to your computer and use it in GitHub Desktop.
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?
@locks
Copy link

locks commented Dec 9, 2014

Option 2.

@cristianrasch
Copy link

+1 for option #2

@tak1n
Copy link

tak1n commented Dec 9, 2014

Option 1

@godfat
Copy link

godfat commented Dec 9, 2014

Option 2.

@bjonord
Copy link

bjonord commented Dec 9, 2014

option 1.

@spacecowb0y
Copy link

Option 1 looks cleaner than option 2 BUT option 2 seems to be the right way to do it.

+1 for option 2!

@plexus
Copy link
Author

plexus commented Dec 9, 2014

Finally the hipster option

def ←x(x)
  self.class.new(x: x, y: @y)
end

@plexus
Copy link
Author

plexus commented Dec 9, 2014

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.

@DimaD
Copy link

DimaD commented Dec 9, 2014

#3 and #4 :)

@moonglum
Copy link

moonglum commented Dec 9, 2014

Option #4, but I would use copy to make it obvious that you get a new object back 😄

@tak1n
Copy link

tak1n commented Dec 9, 2014

@plexus, reading your comments about it made me switch from #1 to #3 😄

@machisuji
Copy link

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))

@plexus
Copy link
Author

plexus commented Dec 10, 2014

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment