partial.lenses

repository·master·Indexed 21 days ago

https://github.com/calmm-js/partial.lenses

A high-performance optics library for JavaScript (version 14.17.0) providing lenses, isomorphisms, and traversals for querying and updating immutable data structures. Optimized for JSON manipulation, it allows users to view, insert, update, and remove data, as well as define recursive optics and conditional paths. The library includes a comprehensive set of utilities for composing optics, managing indices, and collecting or folding values from traversals.

Tokens
37.7K
Snippets
227
Records
266
Agent score
76%

What's inside partial.lenses

  1. Performance characteristics and design principles

    master

    Partial Lenses is optimized for high performance with several key design choices:

    • Static Land Compliance: Follows the Static Land specification rather than Fantasy Land. This avoids wrapping values in objects (like Just), reducing memory allocations and overhead.
    • Direct Optic Values: Allows strings (for properties) and non-negative integers (for indices) to be used directly as optics to avoid unnecessary closure allocations.
    • Array Composition: Treats arrays of optics as a composition of optics, allowing for highly efficient path manipulation.
    • JSON Focus: Optimized for direct manipulation of JSON-compatible data structures to avoid conversion overhead to specialized immutable collections.

    Note on Benchmarks: The library is designed to minimize overhead, avoid stack overflows, and avoid quadratic algorithms. Users should measure their specific use cases, as performance varies by operation type.

  2. Build a Binary Search Tree (BST) lens

    master

    Complex data structures like BSTs can be modeled using lenses by combining L.cond for dynamic branch selection and L.lazy for recursion. To maintain structural integrity (like BST properties) after removals, use L.rewrite to transform the tree after a modification.

    // A recursive search lens using L.cond and L.lazy
    const search = key =>
      L.lazy(rec => [
        naiveBST,
        L.cond(
          [n => !n || key === n.key, L.defaults({key})],
          [n => key < n.key, ['smaller', rec]],
          [['greater', rec]]
        )
      ])
    
    const valueOf = key => [search(key), 'value']
    
    // Using L.rewrite to fix tree structure after removal
    const naiveBST = L.rewrite(n => {
      if (undefined !== n.value) return n
      const s = n.smaller, g = n.greater
      if (!s) return g
      if (!g) return s
      return L.set(search(s.key), s, g)
    })
    
    // Example usage with removal
    const sampleBST = fromPairs([[3, 'g'], [2, 'a'], [1, 'm'], [4, 'i'], [5, 'c']])
    L.remove(valueOf(3), sampleBST)
  3. What are optics and why use them?

    master

    Optics provide a way to decouple the operation you want to perform on data from the logic required to select specific elements and the logic required to maintain the integrity (invariants) of the data structure.

    By using optics, you can express selection algorithms and data structure invariant maintenance as a composition of optics. This allows you to reuse those optics across many different operations, leading to more robust and concise code compared to writing manual getter/setter functions for every specific data shape.

  4. What are Partial Lenses?

    master

    Partial Lenses are an abstraction used to simultaneously specify operations to update and query immutable data structures.

    This library provides a collection of optics, which include:

    • Isomorphisms: Mapping between different data representations.
    • Lenses: Focusing on specific parts of a data structure.
    • Traversals: Navigating through multiple parts of a structure.

    Partial Lenses specifically allow you to:

    • View optional data.
    • Insert new data.
    • Update existing data.
    • Remove existing data.
    • Provide defaults and maintain required parts of a data structure.

    While optimized for JSON, you can write new optics for non-JSON objects, such as Immutable.js collections.

  5. Performance: Nesting traversals does not create intermediate aggregates

    master

    Unlike standard functional programming approaches (like Ramda's R.map or R.filter) which create intermediate arrays at every step of a composition, partial.lenses traversals do not materialize intermediate aggregates. This makes them highly efficient for processing large datasets.

    Example Comparison:

    Using standard composition:

    // Creates intermediate arrays for flatten, map, and filter
    const sumPositiveXs = R.pipe(R.flatten, R.map(R.prop('x')), R.filter(R.lt(0)), R.sum)

    Using partial.lenses traversals:

    // No intermediate arrays are created
    L.sum([L.flatten, 'x', L.when(R.lt(0))], sampleXs)
    L.sum([L.flatten, 'x', L.when(R.lt(0))], sampleXs)
    // 3
  6. Summary of optic forms and operations

    master

    The library provides various ways to compose and transform data using optics. The following table summarizes the available forms and their mathematical semantics:

    FormOperation(s)Semantics
    NestingL.compose(...optics) or [...optics]Monoid over unityped optics
    RecursingL.lazy(optic => optic)Fixed point
    AdaptingL.choices(optic, ...optics)Semigroup over optics
    QueryingL.choice(...optics) and L.chain(value => optic, optic)MonadPlus over traversals
    PickingL.pick({...prop:lens})Product of lenses
    BranchingL.branch({...prop:traversal})Coproduct of traversals
    SequencingL.seq(...transforms)Monad over transforms
  7. Handle missing data with valueOr and find

    master

    Partial lenses are designed to handle missing data gracefully. If L.find fails to locate an element, the lens can behave as an instruction to append a new element. You can use L.valueOr(defaultValue) to ensure that querying non-existent data returns a specific default instead of undefined or null.

    // If 'fi' doesn't exist, L.valueOr ensures we get the default value
    L.get(textIn('fi'), sampleTitles) // returns ''
    
    // Works even if the source object is undefined
    L.get(textIn('fi'), undefined) // returns ''
  8. Understand the concept of Optics in partial.lenses

    master

    The library provides abstractions known as optics. These are used to focus on parts of data structures.

    Core Abstractions:

    • Traversals: Can target any number of elements.
    • Lenses: A restriction of traversals that target exactly one element.
    • Isomorphisms: A restriction of lenses that includes an inverse.

    Key Design Principle: Partiality Unlike 'total' functions that must be defined for all inputs, these optics are partial. If an input does not match the expectation of an optic, the input is treated as undefined.

    • Reading: Returns undefined if the path doesn't exist.
    • Writing: Replaces the focus with the written value (effectively performing an insertion if the path was missing).

    This partiality allows optics to seamlessly support both insertion and removal and makes compositions more concise.

  9. What are isomorphisms and how to use them

    master

    An isomorphism is a lens that maps between two different data structures (e.g., a flat object and a nested object) such that the mapping is reversible.

    In this library, there is no strict type distinction between partial lenses and isomorphisms, meaning combinators like L.pick can create isomorphisms. However, note that some optic composition patterns (like adapting or querying) may not work as expected on inverted isomorphisms.

    To view through an isomorphism in the inverse direction, use L.getInverse.

    // L.getInverse(iso, data) is equivalent to L.set(iso, data, undefined)
    
    const expect = (p, f) => x => (p(x) ? f(x) : undefined)
    const offBy1 = L.iso(expect(R.is(Number), R.inc), expect(R.is(Number), R.dec))
    
    L.getInverse(offBy1, 1)
    // 0
    
    // Using getInverse with a partial lens to construct a minimal structure:
    L.getInverse('meaning', 42)
    // { meaning: 42 }
  10. Comparing manual data operations vs optics

    master

    Without optics, performing operations on nested data structures often requires writing a collection of specific functions like getText, setText, addText, and remText. These functions must manually handle the path to the data and the logic for updating the structure (e.g., using R.assoc, R.filter, or R.append).

    With partial optics, you separate the selection (how to find the data) and invariant maintenance (how to update the structure correctly) from the operation (what to do with the data).

    // Example of manual operations without optics
    const getEntry = R.curry((language, data) =>
      data.titles.find(R.whereEq({language}))
    )
    const hasText = R.pipe(
      getEntry,
      Boolean
    )
    const getText = R.pipe(
      getEntry,
      R.defaultTo({}),
      R.prop('text')
    )
    const mapProp = R.curry((fn, prop, obj) =>
      R.assoc(prop, fn(R.prop(prop, obj)), obj)
    )
    const mapText = R.curry((language, fn, data) =>
      mapProp(
        R.map(R.ifElse(R.whereEq({language}), mapProp(fn, 'text'), R.identity)),
        'titles',
        data
      )
    )
    const remText = R.curry((language, data) =>
      mapProp(R.filter(R.complement(R.whereEq({language}))), 'titles')
    )
    const addText = R.curry((language, text, data) =>
      mapProp(R.append({language, text}), 'titles', data)
    )
    const setText = R.curry((language, text, data) =>
      mapText(language, R.always(text), data)
    )
  11. What are Partial Lenses and why use them?

    master

    Partial Lenses is a high-performance optics library for JavaScript designed to manipulate JSON data structures. Unlike 'total' lens libraries (like older versions of Ramda) which might throw errors when accessing non-existent paths, Partial Lenses are designed to be partial.

    This means optics can seamlessly:

    • View existing elements.
    • Insert new elements.
    • Replace elements with different types.
    • Remove elements.

    By making all optics partial, the library provides full CRUD (Create, Read, Update, Delete) semantics within a compositional framework. It uses undefined to represent nothingness, which avoids the ambiguity of null in JSON data.

  12. How lenses work: Get and Set

    master

    Lenses act as a DSL for querying and manipulating data. You can compose paths using L.compose(...lenses) (or the shorthand array syntax [...]) to focus on a specific element. Once focused, you can use L.get to retrieve the value or L.set to update it. All operations treat the data as immutable, returning a new object with the changes applied.

    // Focus on the first element of the 'titles' array
    const firstTitleLens = L.compose(
      L.prop('titles'),
      L.index(0)
    )
    
    // Get the value
    L.get(firstTitleLens, sampleTitles)
    
    // Set the value (returns a new object)
    L.set(firstTitleLens, 'New title', sampleTitles)