Search code examples
javascriptfunctional-programminglenses

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


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.


Solution

  • 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.