#311 Immutable setters

jodastephen Wed 23 Jul 2008

How do you change a field of an immutable object?

class Line {
  const Int startX
  const Int startY
  const Int endX
  const Int endY
}

line := Line(10, 20, 90, 100)
derived = Line(line.startX, line.startY, line.endX, 200)

I may have missed something, but this would appear to be the position in Fan today - to change one field in a const object, the caller must manually pass in all the fields. This is ugly, especially as immutability is going to be a big feature in Fan.

There is a close solution in Fan today using a copy constructor:

derived = Line(line) {endY = 200}

However, a class can't define both a copy constructor and a normal constructor and have the new shorter constructor calling syntax call the right one, thus this would actually be:

derived = Line.makeCopy(line) {endY = 200}

The second possible solution is to manually write "setter" type methods:

Line withStartX(Int startX) {
  return Line(startX, startY, endX, endY)
}
derived = line.withEndY(200)

Not bad, but still boilerplate, and the kind that proper properties needs to handle.

So, I'd like to consider a language change.

What if you could call a "setter" on a const field, but it returns a new instance of the parent:

derived = line.endY = 200
derived = (line.endY = 200)

Not ideal, as the first syntax option already means something in Fan, and the second syntax isn't really helping.

Maybe we need a "immutable assign operator", which pushes the value back into a new copy of the parent. (made up syntax)

derived << line.endY = 200
derived = line.endY = 200

Or if you just wanted to change the variable line:

line<<.endY = 200
// syntax sugar operator for
line << line.endY = 200

(I'm really not sure about this syntax, ideas welcome...)

With this change you would allow const fields to have declared "setters". They would act like the withXxx method I wrote above, and would validate the value being passed in.

I think fields are a key feature of Fan, but this is one example where there are still a few rough edges to work out.

BTW. This is another solution to our construction issue: First construct the object without the construction-with-block, then apply the changes in the block by changing each value using the const "setter". Internally this might create lots of objects that are immediately thrown away (possibly optimisable to not do this) but that is rarely a big issue for the GC. For example:

line := Line(0, 10) {endX = 50; endY = 60}
// becomes psuedo-code:
// line := Line(0, 10) - fixed const state
// line<<.endX = 50 - new line instance with new state
// line<<.endY = 60 - new line instance with new state

And no need for a new-handler, as the const "setters" work and validate just like normal setters.

Update: Another possible syntax:

line.endY <<= 200
// so you could also do
line.endY <<+= 200

helium Wed 23 Jul 2008

Sounds like OCaml's "with".

let foo = { startX = 10, startY = 20, endX = 90, endY = 100 }
let bar = { foo with endY = 200 }

tompalmer Wed 23 Jul 2008

I saw people trying to work this out in Scala land, too. I think some clean feature along this lines would be really good.

brian Wed 23 Jul 2008

My current thinking was to support this with Obj.dup (which doesn't exist today). Then the compiler will treat a call to dup as a constructor and allow setting of const fields (since we can safely guarantee we are constructing a new object):

derived := line.dup { endY=200 }

Another good reason to make with-blocks flexible and just rely on compiler checks to determine when something is safe or not to set.

jodastephen Wed 23 Jul 2008

"dup" sounds very compiler like ;-)

One thing that an operator could allow is setting const objects within mutables objects:

profile.person.telephone<<.code <<= "+44"

The operators push the immutable change down until there is a mutable object which can be changed.

tompalmer Wed 23 Jul 2008

I have to save that profile.person.telephone<<.code <<= "+44" is really hard for me to understand. I get that dup becomes magical, but at least it looks clear. Is there any sane way to allow saying this?

profile2 := profile.dup {
  person.telephone.code = "+44"
}

I think Scala uses tree walking and pattern matching to replace things deep in const trees.

brian Wed 23 Jul 2008

Is there any sane way to allow saying this

Maybe - but for the short term I'd say you have to dup all the way up manually. I think if you have a tree of data structures that deep you probably shouldn't be using immutable. I think it is ok to use mutable data structures, just not between threads.

So I guess my take is we can tackle that when we have some real use cases, but I haven't run across that myself yet. Some of the concurrency in Flux will be a good test case.

jodastephen Thu 24 Jul 2008

OK, so, I want to keep this simple, but setting immutable fields is essential for any language that emphasises immutability like Fan. An operator feels natural to me, but not to others.

I quite like the idea of reusing the with block syntax if it can be made to work. Perhaps its time to resurect .new?

line := Line(0, 10, 50, 60)
line = line.new { endX = 80 }

This could make for a simple rule, .new creates a new object:

// construct by method
line := Line(0, 10, 50, 60)
// syntax sugar for
line := Line.new(0, 10, 50, 60)

// construct by with-block
line := Line {startX = 0; startY = 10; endX = 50; endY = 60}
// syntax sugar for
line := Line.new {startX = 0; startY = 10; endX = 50; endY = 60}

// clone and alter by with-block
line2 = line.new { endX = 80 }

// clone and don't alter
line2 := line.new

This feels less magical and more natural than .dup.

JohnDG Thu 24 Jul 2008

Looks quite nice, especially if it can handle data nested arbitrarily.

In the UNA codebase, we have tons of immutable structures. They all follow a pattern:

class T {
   private final XXX xXX;

   T withXXX() {
   }
}

Or for collections:

class T {
   private final Collection<T> elements; // Collections.unmodifiable*
   // other immutable fields

   T withMoreElements(Collection<T> elements) {
   }

   T withoutElements(Collection<T> elements) {
   }

   // And singleton equivalents
}

Updating nested immutable objects becomes a real pain.

brian Fri 25 Jul 2008

OK, so, I want to keep this simple, but setting immutable fields is essential for any language that emphasizes immutability like Fan

I think you very likely could be right - but I haven't done enough concurrent programming (in Fan) to experience that first hand. Right now my experience has been more along the lines of mutable structures that I want to freeze to send to another thread (and vise versa). The way List.ro and List.rw work has been more along my lines of thinking.

But I think this use case is quite important to keep in mind as we search for the perfect elusive with-block, const, construction design.

In the UNA codebase, we have tons of immutable structures

Are you actually trying to use immutable structures to parse/analyze the code for code completion? I'm curious to your design, since I'm getting ready to tackle that problem using Fan's concurrency model.

jodastephen Fri 25 Jul 2008

I haven't done enough concurrent programming (in Fan) to experience that first hand

I would like to use immutables much more irrespective of whether I'm planning them to be shared concurrently or not. I don't think I'm alone in this. Making immutables almost as easy to use as mutables would be a huge attraction I think.

BTW, I'd be interested where John got the withXxx() design from - I use it in Joda-Time, and I'd love to know the history of the pattern (with instead of set). After all, I can't imagine Joda-Time invented it, even though I did document it.

JohnDG Fri 25 Jul 2008

Are you actually trying to use immutable structures to parse/analyze the code for code completion? I'm curious to your design, since I'm getting ready to tackle that problem using Fan's concurrency model.

UNA's real-time collaborative merging algorithm is built entirely on immutable data structures.

Code completion uses a tree, and while the tree itself is not immutable, the "tokens" it operates on are immutable (not tokens in the normal sense -- these tokens represent arbitrary patterns).

Documents are represented internally as a series of tokens placed on a token canvas. The tokens are immutable, and listeners receive notification when a token is added, removed, changed (i.e. becomes a different kind of token, in which case the listeners receive both the old and new token), or shifts around (due to typing on the same line).

The code base has numerous examples of immutable structures. Virtually all data structures are immutable -- from file system objects to database objects. They all follow exactly the same pattern noted above.

For some of these objects, there is no public constructor. Rather, the class defines a static NULL field, which represents a null object. To create the kind of object you want, you can simply use NULL.withXXX().withYYY(), etc. There are also static TEST and sometimes static UNKNOWN fields as well.

JohnDG Fri 25 Jul 2008

BTW, I'd be interested where John got the withXxx() design from - I use it in Joda- Time, and I'd love to know the history of the pattern (with instead of set). After all, I can't imagine Joda-Time invented it, even though I did document it.

It's called the Immutable design pattern, and although I thought we were the first to use the withXXX convention, I have seen other examples of it as well (obviously not in Java -- look at how much grief String.replace causes newbie Java developers). For a speaker of English, with seems an obvious choice, because you want object X WITH some other value of a particular field Y.

In other words, you want:

X with Y

=>

X.withY()

I'd love for Fan to have first-class support for changing arbitrarily nested immutable data objects. Maybe a with-block, but with keyword .with.

foo.with { a = b }

jodastephen Fri 25 Jul 2008

I thought we were the first to use the withXXX convention, I have seen other examples of it as well

I think its becoming more common. Joda-Time used it from about 2002, and JSR-310 will also use it.

In Fan, a .with might work, but it would need to be associated with construction too ideally:

foo.with { a = b }

foo := Foo {a = b}
// syntax sugar for 
foo := Foo.with{a = b}
// syntax sugar for
foo := Foo()
foo.with {a = b}

I'm not sure whether I prefer this option or the .new option.

JohnDG Fri 25 Jul 2008

It seems like with blocks for immutable structures should have a different syntax than for mutable structures. Otherwise, a developer will not know by looking at a code if he is changing a mutable structure or creating a clone structure.

The one case where it is safe to overlap mutable syntax with immutable syntax is construction, because the effect is the same regardless.

brian Sat 26 Jul 2008

t seems like with blocks for immutable structures should have a different syntax than for mutable structures. Otherwise, a developer will not know by looking at a code if he is changing a mutable structure or creating a clone structure.

Actually I think this might be important in the whole ctor/with-block design. I have not agreed that with-block setting const fields should have a different syntax (anymore than I think they should use some special assignment operator). However setting a mutable object in place and creating a new object are very different and probably should have different syntax. Maybe that is the distinction some of you were making with constructors already (but John's statement shifted my perspective).

What I really like about all of these discussions is that our use cases keep increasing for with-blocks/constructors/etc and giving us a much broader background to judge our design proposals against. That's all good!

dobesv Mon 30 Jan 2012

For the nested data structures I think you could probably automatically transform:

a.with{ b.c.d = 1 }

into

a.with { b = b.with { c = c.with { d = 1 } } }

The last place where I was mucking around with a lot of recursive immutable data structures was an experiment in Scala making a typed document data structure.

The major payoff of using immutable data structures is the ability to use pattern matching to walk the object tree - Scala doesn't allow pattern matching against regular objects, only immutable ones. So a recursive walk over the object tree was a lot easier using the immutable objects.

Now, if Fantom supports pattern matching against mutable objects that might be a different story. However, I do think that using immutable objects and programming in "functional style" has many benefits ... but only the language makes it easy enough.

lae Tue 14 Feb 2012

I like jodastephen's idea of (or something close to this):

line.new { endX = 80 }

That's beautifully simple.

dsav Tue 14 Feb 2012

Doesn't Fantom have enough syntax sugar?

There is some wisdom in that it's difficult to change a field in a const object. It's const. You should rarely want to change it. And if you need to do so (i.e. spawn new object with one field changed), there should be logic behind such operation.

line := Line(10, 20, 90, 100)
derived = Line(line.startX, line.startY, line.endX, 200)
  • this makes little sense for me (what's so derived in derived line?).
line := Line(10, 20, 90, 100)
extended = line.extendY(100)
  • this makes much more sense and requires no addition syntax sugar.

SlimerDude Tue 14 Feb 2012

Is this not what the Obj#with() method is for? To allow you to write:

blogs := Coor(10, 15)
fred  := blogs.with { y = 100 }

with Coor looking like

class Coor {

  Int x; Int y;

  override This with(|This| b) { makeModify(this, b) }

  new makeModify(Coor? orig, |This| b) {
    if (orig != null) {
      this.x = orig.y
      this.y = orig.y
    }
    b(this)
  }
}

brian Tue 14 Feb 2012

Is this not what the Obj#with() method is for? To allow you to write:

Yeah you can roll your own fairly easily using it-blocks. Although I'd probably do using a constructor like this:

new makeFrom(Foo orig, |This| f)
{
   // copyfields orig ->this
   f(this)
}

For the record though, I design a majority of my classes as immutable and I can't ever remember really wanting to do this.

SlimerDude Tue 14 Feb 2012

(The following Q is probably a little off topic but...)

So what is the Obj#with() method for? I mean, in what circumstance would I call or override it?

brian Tue 14 Feb 2012

So what is the Obj#with() method for? I mean, in what circumstance would I call or override it?

You basically use it to access your object with an it-block, often when you don't have an it-block constructor. Here is a common pattern I'll often use for it:

list := Str[,] { capacity = 100 }

Login or Signup to reply.