Skip to content

Instantly share code, notes, and snippets.

@stuartwakefield
Last active April 5, 2016 08:18
Show Gist options
  • Save stuartwakefield/d23e40f52eeb8c3ea8b5a5a6e681a587 to your computer and use it in GitHub Desktop.
Save stuartwakefield/d23e40f52eeb8c3ea8b5a5a6e681a587 to your computer and use it in GitHub Desktop.
JavaScript reconciling immutability, lenses with loose typing

Reconciling immutability, lenses with loose typing in JavaScript

WIP

How do we model functions and state, i.e. object orientation in FRP. When JavaScript is loosely typed and we want nice things like immutable updating and lenses. Lenses have to be able to make a copy of the object that has changed.

(Look at shapeless lens https://github.com/milessabin/shapeless to see what is meant.)

One way is to accept all internal state for an object through the constructor, this means being able to recreate an object in any given state. If the class is truly immutable there will be little choice but to do this anyhow.

Why is immutability useful?

JavaScript is single threaded and so the benefits of having the safety of an object with guaranteed immutablility when accessing from different threads isn't a benefit that translates to JavaScript. What is relevant is repeatability, illustrated with the following example:

a = Pos(2, 3)
> Pos
    x: 2
    y: 3

// setX returns a new Pos with the
// new state...
a.setX(a.x * 2)
> Pos
    x: 4
    y: 3

// The same arguments applied to the
// same object yield the same results
// as before.
a.setX(a.x * 2)
> Pos
    x: 4
    y: 3

Instead of with mutable objects, repeatability is lost...

a = Pos(2, 3)
> Pos
    x: 2
    y: 3

// Set a updates the x member internally
// here we have allowed the setX method
// to return this...
a.setX(a.x * 2)
> Pos
    x: 4
    y: 3

// Performing the call again updates the
// internal state further.
a.setX(a.x * 2)
> Pos
    x: 8
    y: 3

We could say that mutable actions are destructive as we have lost information about the intermediate states.

How does this translate to larger domains?

class Pos {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  
  setX(x) {
    return new Pos(x, this.y)
  }
  
  setY(y) {
    return new Pos(this.x, y)
  }
}
class Player {
  constructor(pos) {
    this.pos = pos
  }
  
  setPos(pos) {
    return new Player(pos)
  }
  
  // Sugar methods
  setX(x) {
    return new Player(this.pos.setX(x))
  }
  
  setY(y) {
    return new Player(this.pos.setY(y))
  }
}
player = Player(Pos.ZERO)
> Player
    x: 0
    y: 0
player.setPos(player.pos.setX(123).setY(987))
> Player
    x: 123
    y: 0
// Using the sugar method
player.setX(123).setY(987)
> Player
    x: 123
    y: 987
// Original unchanged
player
> Player
    x: 0
    y: 0
// Using lenses
posYLens.set(posXLens.set(player, 123), 987)
> Player
    x: 123
    y: 987
class Game {
  constructor(players) {
    this.players = players
  }
  
  setPlayers(players) {
    return new Game(players)
  }
}
// Ideally performing a map operation on the
// players member of game should return a new
// game instance with the updated players array.
// Move every player x + 10
game.setPlayers(game.players.map(player => player.setX(player.x + 10)))
> Game
    players: [...]

What about deep hierarchies / nesting and performance related costs.

Pos has two members x and y and two setter functions setX and setY, Player has one member with a setter function pos and setPos, with sugar methods reaching in one level. Game has one array member and setter function players and setPlayers. In a mutable system our update above might look like:

game.players.forEach(function (player) {
  player.setX(player.x + 10);
});

Note that we are relying on mutability to retain the updates. Performance of the latter will be much better than the former. In the former we have a new Pos object and a new Player object for every player in the game players list, plus a new Game object. In the latter, there are no additional objects created beyond the primitive numerical values.

Note that although the immutable version is less performant it is not quite as bad as performing a full copy on the entire structure, this is because where objects have not changed they are shared with the new structure. Because the structure is immutable this is a safe operation... Conversely, if we allow mutability, this is not safe:

var a = new Game([ new Player(new Pos(10, 0)), new Player(new Pos(20, 0) ])
> Game
    players: [
      Player
        x: 10
        y: 0
      Player
        x: 20
        y: 0
    ]

// We only change one of the players    
var b = a.setPlayers(a.players.map(player => player.x === 10 ? player.setX(player.x + 10) : player))
> Game
    players: [
      Player
        x: 20
        y: 0
      Player
        x: 20
        y: 0
    ]

// Changing the second player in b also affects
// the player in a because they are sharing the
// object instance.
b.players[1].pos.x = 30
a
> Game
    players: [
      Player
        x: 10
        y: 0
      Player
        x: 30
        y: 0
    ]

We can use Object.defineProperty(object, name, props) to indicate immutability:

Object.defineProperty(this, 'x', {
  value: x,
  enumerable: true,
  writable: false,
  configurable: false
})

Or Object.freeze(object):

return Object.freeze(this)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment