Is it possible to model update with constrains with Lens (or other optics)?

160 views Asked by At

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:

  1. abstract actual state representation from user update logic (i.e. component accept generic state T and Lens<T, number> works)

  2. composing multiple constraints (rulers) seems just like lifted version of composing lenses. There are actually other constrains like min, max constraint on start and end. 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?

0

There are 0 answers