Lenses: A way to uncouple shared state
We’ve touched on the concept of lenses before [here, here]. This post aims to cover the concept in detail and offer a general programmatic implementation.
Lenses are a generic way to handle shared state. Shared state is some piece of state (member variable, static variable or whatever) that is mutated by more than one process (module, piece of code, thread, etc.) and observed by one or more processes. The term shared state is often used to describe things mutated by multiple operating system processes (threads). It also applies to situations where a program’s internal processes can be thought of as unordered; for instance, when processes are dependent on incoming user input events or when using a time slicing mechanism (as in update functions per entity in a video game). Three examples of shared state in a video game might be:
- Is the game paused? This value might be mutated by a cutscene, tutorial, disconnected controller, or menu.
- What actions can the player take (e.g. use grenade, cast spell, jump, fly)? This value might be mutated by items, progress, tutorials, etc.
- How much damage does the player do? This value might be mutated by a variety of items, temporary power ups, levelling, etc.
The key thing here is that the values or sets of values are changed by multiple processes in your application, and then read by one or more processes. This example code illustrates the type of bug shared state typically leads to:
class Game{
static bool paused;
}…tutorial_prompt(){
Game.paused = true;
show_tutorial().then(
() => Game.paused = false
);
}…clicked_menu(){
Game.paused = true;
open_menu();
}clicked_exit(){
Game.paused = false;
close_menu();
}
Here we have a tutorial prompt that sets the game to paused; we also have functions that open and close the menu, which set the game to paused or unpaused. What happens if the menu is opened and then the tutorial kicks off? Well, when the tutorial prompt closes the game will become unpaused and the menu will still be open. This is a contrived example but a very common problem nonetheless. A simple solution might look something like:
tutorial_prompt(){
Game.paused = true;
show_tutorial().then(
() => Game.paused = Menu.open? true:false
);
}
…clicked_menu(){
Game.paused = true;
open_menu();
}clicked_exit(){
Game.paused = Tut.open? true:false;
close_menu();
}
We check to see if the menu was open and don’t set paused back to false if it was. Easy.
But wait! Now the tutorial system knows about the fact that there are menus in the game and vice versa. We also had to expose the menu and tutorial open states as public properties. If we remove either of those features from the game or change them significantly we’ll need to edit the others. Add in a cutscene or input system who can also pause the game and you can see we have a problem (n bool checks at each read of pause). This is called coupling, and it’s poopey (a very technical term) for your project.
Here’s one more example that comes up a lot in games: How much damage does the player do? Often we start by describing the damage as a simple floating point number. Then when you get an item you add to it or multiply it. Pretty soon we figure out that the order in which additions and multipliers are applied matters and then we start sorting items by the type of damage they do. Then we decide you might also get buffs so we need to make sure that the buffs are also sorted into that list and so on.
Let’s look at how lenses solve both of these problems. Conceptually in both of these cases you are dealing with a value (a bool, a list, a float or whatever) that has multiple observers (processes that want that value at an arbitrary time) and multiple mutators (processes that statefully affect the value at an arbitrary time).
Some of the existing solutions for dealing with shared state are semaphores (resource use locks) and reactive programming.
Semaphores work really well and are efficient but only work for problems that can be modelled as locking resources (like the pause problem, called idempotent problems). They don’t work for problems where ordering of the mutator’s actions matters or in problems where the value can be modified in arbitrary ways by a mutator.
With reactive programming, modules receive events whenever a value is modified, and observers get notifications (and usually a reference to who modified it). Usually the observers then do some internal bookkeeping to track the state of the variable. This sort of code often ends up being much more complicated and has a number of variables local to the modules that track the shared state in some way.
So what’s a lens?
Lets call a piece of shared state a lensed value. Imagine the lensed value as a physical object and the mutating modules as lenses. When you look through the lens between at the object, it looks different than it did by itself. What’s more, you can stack multiple lenses in different configurations and they all contribute to a different representation of the object.
Lens ordering and how a lens transforms the state is dependent on the specific application. Lenses can be generally implemented as an ordered collection of transformations to a state variable. Many specific cases can be implemented more efficiently.
How? Here’s a generic C# implementation:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;// This is the part mutating modules apply to the LensedValue
public class Lens<T>{
public int priority;
public Func<T,T> transformation; public Lens(int priority, Func<T,T> transformation){
this.priority = priority;
this.transformation = transformation;
}
}
Then we have the LensedValue class itself:
// This class represents the shared state and has the GetValue()
// method that returns the value rendered through the lenses.
class LensedValue<T> {T value;
List<Lens<T>> lenses;
public LensedValue(T initialValue){
this.value = initialValue;
this.lenses = new List<Lens<T>>();
}
public LensToken AddLens(Lens<T> lens){
lenses.Add(lens);
lenses.Sort((x,y)=> x.priority - y.priority);
return new LensToken(
() => lenses.Remove(lens)
);
}
public T GetValue(){
T tmp = value;
foreach(var lens in lenses){
tmp = lens.transformation(tmp);
}
return tmp;
}
}
and LensToken for removing lenses when they are no longer useful:
// LensToken is a simple class used to keep a token to remove
// a Lens from a LensedValue
class LensToken{
Action action;
public LensToken(Action action){
this.action = action;
}
public void Remove(){
action();
}
}
And here’s an example of how you might use it for the toy problem of tracking a player’s dealt damage we discussed earlier:
class Player{
public static LensedValue<float> damage =
new LensedValue<float>(10);
}public class LensExampleDamage : MonoBehaviour {enum DamageType{
Base = 0,
Multiplier = 1,
Percentage = 2
}void Start () {
Debug.Log(Player.damage.GetValue()); // 10
var multDmgLensToken = Player.damage.AddLens(
new Lens<float>(
(int)DamageType.Multiplier, (dmg)=> dmg * 2)
);
Debug.Log(Player.damage.GetValue()); // 20
var baseDmgLensToken = Player.damage.AddLens(
new Lens<float>(
(int)DamageType.Base, (dmg)=> dmg + 5)
);
Debug.Log(Player.damage.GetValue()); // 30
var multDmgLensToken2 = Player.damage.AddLens(
new Lens<float>(
(int)DamageType.Multiplier, (dmg)=> dmg * 1.5f)
);
Debug.Log(Player.damage.GetValue()); // 45 multDmgLensToken.Remove();
Debug.Log(Player.damage.GetValue()); // 22.5
}
}
Of course this is a toy example (the two other examples discussed above are here and here), but it shows how easily multiple modules can mutate and read a LensedValue without concern for each other.
There are some obvious extensions we can make:
- use atomic operations to add/remove lenses, so many threads can add lenses without blocking reads or corrupting data
- add optional caching of the LensedValue for Lenses that only use pure functions for their transformations.
- add special types of LensedValue (say IdempotentLensedValue) to support particular optimizations (i.e. if lens rendering order doesn’t matter or mutations can be replaced with a simple counter as in the pause example).
To summarize, we’ve looked at Lenses as a way of decoupling shared state between software processes, and a few examples from game development. There’s lots of improvements and optimizations to make, and I’d love for you to share yours with the community!
If you have any comments or questions, feel free to post here or get in touch on Twitter and if you need some help with a technical project, check out Twisted Oak.