I'm working with an Web UI which requires validation on user inputs. When implementing an double thumb slider, and comes problem when using Lens with a 'constraint' update. When use lens.set
, it is required to validate the new value, and if it is not valid up to a given constraint, a closest valid value should be updated instead. The validation is a composition of multiple simple rulers.
I can't find a proper lens to model this 'decorated' kind of set
or modify
.
I'm using following models, (using monocle-ts)
import { Lens } from "monocle-ts";
interface State {
start: number;
end: number;
}
// assuming start < end always holds
const center = ({ start, end }: State): number => (start + end) / 2;
const length = ({ start, end }: State): number => end - start;
const startLens: Lens<State, number> = Lens.fromProp<State>()("start");
// Models direct drag start (left) thumb
const endLens: Lens<State, number> = Lens.fromProp<State>()("end");
// Models direct drag end (right) thumb
const centerLens: Lens<State, number> = new Lens<State, number>(
center,
(newCenter) => (s) => {
const l = length(s);
return { start: newCenter - l / 2, end: newCenter + l / 2 };
}
);
// Models direct drag slider, i.e. move the 'track' of two thumb slider, only chage center while prserve length.
const lengthLens: Lens<State, number> = new Lens<State, number>(
length,
(newLength) => (s) => {
const c = center(s);
return { start: c - newLength / 2, end: c + newLength / 2 };
}
);
// Models scale the range, change length while preserve center
Every things works fine before there comes a constraint on length. I want to get something like 'step length', thus the length of state is required to be integer multiple of a given step. A first try is something like the following:
const lengthStepLens = (step: number): Lens<State, number> =>
new Lens<State, number>(length, (newLength) => (s) => {
const stepLength = Math.round(newLength / step) * step;
const c = center(s);
return { start: c - stepLength / 2, end: c + stepLength / 2 };
});
Although it seems work on keep length step constraint satisfied, however the lengthStepLens
above seems breaks lens law:
get(set(a)(s)) = a
e.g. if step = 1
,
const l = lengthStepLens(1);
const s: State = { start: 0.0, end: 1.0 };
const a: number = 0.9;
console.log(l.get(l.set(a)(s))); // prints 1, != 0.9
The law only works on 'valid' new values.
Another approach is use an separate validation and correction logic, but I'm missing two advantage of using lens:
abstract actual state representation from user update logic (i.e. component accept generic state
T
andLens<T, number>
works)composing multiple constraints (rulers) seems just like lifted version of composing lenses. There are actually other constrains like
min
,max
constraint onstart
andend
. composing constrains seems like some 'monadic' chaining of lenses, maybe something like
function merge<S, A>(la: Lens<S, A>, lb: Lens<S, A>): Lens<S, A> {
// assumes:
// forall s, if s is valid on both la and lb, la.get(s) === lb.get(s)
// forall a, s, lb.set(a)(la.set(a)(s)) is still valid on la
return new Lens(
(s) => la.get(s),
(a) => (s) => lb.set(a)(la.set(a)(s))
);
}
Question:
Is a there correct optics with law satisfied for this problem?
What would happen when using lens with laws broken?