Skip to content

RFC: support nested objects in reflect #45

@ap0nia

Description

@ap0nia

Support nesting regular objects inside the returned value from lens.reflect.

Although not necessarily in-scope for lenses, it may be possible to support both lenses and nested objects with lenses from the reflect method by automatically applying the reflect method for anything that isn't a lens instance whenever using focus.

Example

const reflected = lens.reflect((_dictionary, lens) => {
  a: lens.focus('parent.a'),
  b: {
    c: {
      d: lens.focus('parent.b.c.d')
    }
  }
})

Details

After #43 was implemented, nested lenses were supported properly. This requires that all values in the object are also lenses.

const reflected = lens.reflect((dictionary, lens) => {
  a: dictionary.a,
  b: lens.focus('b'),
  c: lens.reflect(() => {
    return {
      d: lens.reflect(() => {
        e: lens.focus(...)
      })
    }
  })
})

I'm fine with this and would appreciate this being documented. However, I happened to be nesting my lenses like this.

I was using JS and the type errors weren't detected. The code just happened to look like it was working fine. So it is actually documented by TS and you'll receive errors for doing this. My implementation with updated typings seems to allow this pattern and generate correct path names.

const reflected = lens.reflect((dictionary, lens) => {
  a: dictionary.a,
  b: lens.focus('b'),
  c: {
    d: {
      e: lens.focus('a.b.c.d.e')
    }
  }
})

Here, c is not a lens, but contains a deeply nested lens. This is significant because this line of code will error because reflected.focus('c') will return the object { d: { e } } instead of another LensCore object.

This differs from the previous implementation that would previously resolve the entire path at once and always return a LensCore object. While it may be out of scope to support nesting anything besides lens, here is a potential implementation to support both strategies.

Implementation

Original source location

lenses/src/LensCore.ts

Lines 68 to 74 in e442fe6

const overriddenLens: LensCore<T> | undefined = get(this.override, propString);
if (!overriddenLens) {
const result = new LensCore(this.control, nestedPath, this.cache);
this.cache?.set(result, nestedPath);
return result;
}

Possible solution

  let overriddenLens: LensCore<T> | Undefined
  
  // The idea is that values in the reflected object may be nested objects or simply lenses at any level.
  type NestedLens = Record<string, LensCore<T> | NestedLens>
  
  const overriddenLensOrRecord: LensCore<T> | NestedLens | undefined = get(this.override, propString);

  // use this.constructor in case this method is being called from a super class?
  if (overriddenLensOrRecord instanceof this.constructor) {
    overriddenLens = overriddenLensOrRecord
  } else if (overriddenLensOrRecord) {
    overriddenLens = this.reflect(() => overriddenLensOrRecord)
  } else {
    const result = new LensCore(this.control, nestedPath, this.cache); 
    this.cache?.set(result, nestedPath); 
    return result; 
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions