How can I delete a focused property of an object with a van Laarhoven Lens?

Viewed 98

With my simple lens implementation i can perform the usual modify, set, get and delete operations:

// Lens type

const Lens = f => ({runLens: f, [Symbol.toStringTag]: "Lens"});

const objLens = map => k =>
  Lens(f => o =>
    map(x => Object.assign({}, o, {[k]: x})) (f(o[k]))); // object lens

// Id type

const Id = x => ({runId: x, [Symbol.toStringTag]: "Id"});
const idMap = f => tx => Id(f(tx.runId)); // functor

// Const type

const Const = x => ({runConst: x, [Symbol.toStringTag]: "Const"});
const constMap = f => tx => Const(tx.runConst); // functor

// auxiliary function

const _const = x => y => x;

// MAIN

const o = {foo: "abc", bar: 123};

const get = objLens(constMap) ("foo").runLens(x => Const(x)) (o),
  set = objLens(idMap) ("bat").runLens(_const(Id(true))) (o),
  mod = objLens(idMap) ("foo").runLens(s => Id(s.toUpperCase())) (o),
  del = objLens(idMap) ("foo").runLens(_const(Id(null))) (o); //*

console.log("get", get.runConst);
console.log("set", set.runId);
console.log("mod", mod.runId);
console.log("del", del.runId);

However, delete is unsatisfying as I want to delete the whole property rather than merely replacing the value with nothing.

How can I achieve this?

*Please note that i usually would have used a proper Option type to indicate no values.

2 Answers

This is what I would do:

// type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a

// newtype Const b a = Const { getConst :: b } deriving Functor
const Const = getConst => ({ getConst, map: _ => Const(getConst) });

// newtype Identity a = Identity { runIdentity :: a } deriving Functor
const Identity = runIdentity => ({ runIdentity, map: f => Identity(f(runIdentity)) });

// remove :: String -> Object -> Object
const remove = key => ({ [key]: _, ...rest }) => rest;

// prop :: String -> Lens Object (Maybe Value)
const prop = key => fun => obj =>
    fun(obj.hasOwnProperty(key) ? { fromJust: obj[key] } : null)
        .map(data => Object.assign(remove(key)(obj), data && { [key]: data.fromJust }));

// get :: Lens a b -> a -> b
const get = lens => data => lens(Const)(data).getConst;

// modify :: Lens a b -> (b -> b) -> a -> a
const modify = lens => fun => data => lens(x => Identity(fun(x)))(data).runIdentity;

// set :: Lens a b -> b -> a -> a
const set = lens => value => modify(lens)(_ => value);

// del :: Lens a (Maybe b) -> a -> a
const del = lens => set(lens)(null);

// foo :: Lens Object (Maybe Value)
const foo = prop("foo");

console.log(get(foo)({ foo: 10, bar: 20 })); // { fromJust: 10 }
console.log(del(foo)({ foo: 10, bar: 20 })); // { bar: 20 }

As seen, the type signature of a property lens like foo is Lens Object (Maybe Value). This makes sense because if you try get(foo)({ bar: 20 }) you should get nothing. The del function works on any lens which focuses on a maybe value, and sets its value to nothing (i.e. null).

Credit goes to Bergi for showing me that it's possible to pattern match on computed properties.

One possible way would be to define a special type that denotes deletion:

const Lens = f => ({runLens: f, [Symbol.toStringTag]: "Lens"});

const objLens = map => k =>
  Lens(f => o => map(x => {
    if (x[Symbol.toStringTag] === "Deleter") {
      const p = Object.assign({}, o);
      delete p[k];
      return p;
    }

    else
      return Object.assign({}, o, {[k]: x});
    }) (f(o[k])));

const Id = x => ({runId: x, [Symbol.toStringTag]: "Id"});
const idMap = f => tx => Id(f(tx.runId)); // functor

const _const = x => y => x;

// deletion type

const Deleter =
  ({get runDeleter() {return Deleter}, [Symbol.toStringTag]: "Deleter"});

// MAIN

const o = {foo: "abc", bar: 123};

const del = objLens(idMap) ("foo").runLens(_const(Id(Deleter))) (o);

console.log("del", del.runId);

However, _const(Id(Deleter)) isn't particularly intuitive, but looks hackish. Hopefully there is a better approach.

Related