Sunday, 14 April 2019

Immutability via Proxies

There are many of the recent programming trends that feel quite odd to me and that I hardly manage to find appealing. Immutability is in part one of them. For sure it has its cases, but it's not easy to me to understand all the hype around it. This said, the other day I read a pretty interesting explanation about Angular, Interceptors and why they designed HttpRequest and HttpResponse as mainly immutable:

Although interceptors are capable of mutating requests and responses, the HttpRequest and HttpResponse instance properties are readonly, rendering them largely immutable.

They are immutable for a good reason: the app may retry a request several times before it succeeds, which means that the interceptor chain may re-process the same request multiple times. If an interceptor could modify the original request object, the re-tried operation would start from the modified request rather than the original. Immutability ensures that interceptors see the same request for each try.

There are multiple libraries and patterns to work with immutable objects. One simple pattern that I saw some time ago was something like this:


class Person
{
        public string Name {get;}
        public int Age {get;}
        
        public Person (string name, int age)
        {
            this.Name = name;
            this.Age = age;
        }
        
        public Person SetName(string name){
            return new Person(name, this.Age);
        }
        
        public Person SetAge(int age){
            return new Person(this.Name, age);
        }
}

//Main
var p1 = new Person("Francois", 25);

var p2 = p1.SetName("Xuan");

Automatic Properties in C# create a private backing field. In JavaScript this pattern becomes a bit more complex as there is no real privacy, so we would have to add some additional technique to the mix, like having the backing fields in a closure...

What seemed interesting to me was creating an immutable wrapper around a normal object. If we could trap the access to set and get operations on the existing object... well, seems like in JavaScript land proxies are a perfect fit!

So the idea is to have a function that receives an object and creates a proxy with that object as target. The Proxy has a set trap that prevents assignments from happening, and a get trap that when receiving an request for a set_XXX property (that indeed is a call to a set_XXX method, as you know in JavaScript method invokation involves 2 steps, getting the function and invoking it) will accept this as a valid assignment, creating a copy of the existing object with the new assigned value and wrapping it in a proxy so that ensuing set operations behave in the same way. The get trap does indeed return a function that when invoked does the cloning, assignment and proxy wrapping.

Well, you better check the code to understand what I'm trying to explain. Voila the function.


function createImmutable(item){
	let handler = {
		set: function(target, property, value){
			//do nothing, this object is no longer "directly" mutable
			console.log("hey, I'm immutable, you can't set me like this");
		},

		//object is mutable only by invoking a "set_XXXX" method
		//we trap that get and return a function that will create the new object with the mutated property
		//and returns a proxy around the new object, so that immutability continues to work in ensuing assignments
		get: function(target, property, receiver){
			if (property.startsWith("set_")){
				let propName = property.substring(4);
				console.log("assigning to " + propName + " via proxy");
				return function(value){
					//either use the trapped target or "this"
					//let newItem = new target.constructor();  
					let newItem = new this.constructor();
					Object.assign(newItem, target);
					//notice I've just doing shallow cloning
					newItem[propName] = value;
					return new Proxy(newItem, handler);
				}

			}
			else{
				return target[property];
			}
			
		}
	};

	return new Proxy(item, handler);
}

And below how to use it:

class Person{
	constructor(name){
		this.name = name;
	}
	
	say(word){
		return `${word}, I'm ${this.name}`; 
  }
}


console.log("started");
let p1 = new Person("Francois");

console.log("p1 says: " + p1.say("hi"));

let immutableP1 = createImmutable(p1);

console.log("immutableP1" + JSON.stringify(immutableP1));
immutableP1.name = "Xuan";
console.log("immutableP1" + JSON.stringify(immutableP1));

let immutableP2 = immutableP1.set_name("Xuan");
console.log("immutableP2" + JSON.stringify(immutableP2));

console.log(immutableP2.say("hi"));

let immutableP3 = immutableP2.set_name("Emmanuel");
console.log("immutableP3" + JSON.stringify(immutableP3));

I've uploaded the code to a gist

No comments:

Post a Comment