funkia / list

🐆 An immutable list with unmatched performance and a comprehensive functional API.
MIT License
1.65k stars 53 forks source link

Access the index on map() operations #88

Open Mistyputt opened 4 years ago

Mistyputt commented 4 years ago

Hi

My use case frequently involves mapping a list of raw data into a UI representation that offers the ability to modify elements. For instance mapping a list into table rows, where the user may modify or delete rows. I was thinking having the index available when mapping would allow me to efficiently update the list on that index.

One could use List.zipWith(f, list, List.range(0,List.length(list))) or avoid the index and use List.find but maybe there is a place for a dedicated mapWithIndex function as that should theoretically be more efficient?

Does anyone else have similar use-cases and what is your solution?

Finally, thanks for a great lib!

Mistyputt commented 4 years ago

I've played around with a naive implementation:

function mapArrayWithIndex<A, B>(f: (a: A, i: number) => B, array: A[], offset: number): [B[], number] {
  const result = new Array(array.length);
  for (let i = 0; i < array.length; ++i) {
    result[i] = f(array[i], offset + i);
  }
  return [result, offset + array.length];
}

function mapNodeWithIndex<A, B>(f: (a: A, i: number) => B, node: Node, depth: number, offset: number, adjust: number): [Node, number] {
  if (depth !== 0) {
    const { array } = node;
    var innerOffset = offset;
    const result = new Array(array.length);
    for (let i = 0; i < array.length; ++i) {
      let [res, newOffset] = mapNodeWithIndex(f, array[i], depth - 1, innerOffset, adjust * 32);
      innerOffset = newOffset;
      result[i] = res
    }
    return [new Node(node.sizes, result), innerOffset];
  } else {
    let [res, newOffset] = mapArrayWithIndex(f, node.array, offset);
    return [new Node(undefined, res), newOffset];
  }
}

function mapPrefixWithIndex<A, B>(f: (a: A, i: number) => B, prefix: A[], length: number): B[] {
  const newPrefix = new Array(length);
  for (let i = length - 1; 0 <= i; --i) {
    newPrefix[i] = f(prefix[i], length - 1 - i);
  }
  return newPrefix;
}

function mapAffixWithIndex<A, B>(f: (a: A, i: number) => B, suffix: A[], length: number, totalLength: number): B[] {
  const priorLength = totalLength - length;
  const newSuffix = new Array(length);
  for (let i = 0; i < length; ++i) {
    newSuffix[i] = f(suffix[i], priorLength + i);
  }
  return newSuffix;
}

/**
 * Applies a function to each element in the given list, and its index, and returns a
 * new list of the values that the function return.
 *
 */
export function mapWithIndex<A, B>(f: (a: A, i: number) => B, l: List<A>): List<B> {
  return new List(
    l.bits,
    l.offset,
    l.length,
    mapPrefixWithIndex(f, l.prefix, getPrefixSize(l)),
    l.root === undefined ? undefined : mapNodeWithIndex(f, l.root, getDepth(l), getPrefixSize(l), 1)[0],
    mapAffixWithIndex(f, l.suffix, getSuffixSize(l), l.length)
  );
}

Maybe one could use the depth and branchingFactor instead but this seems performance-wise to already fare better than zipWith. Here are my rudimentary benchmark results:

List.map 10                   36818486.05 op/s ±  0.54%   (86 samples)
List.mapWithIndex 10          32894014.39 op/s ±  0.39%   (88 samples)
List.zipWith 10               4332197.10 op/s ±  0.63%   (84 samples)
List.map 30000                14337.09 op/s ±  0.71%   (88 samples)
List.mapWithIndex 30000       11587.69 op/s ±  0.54%   (88 samples)
List.zipWith 30000             1240.52 op/s ±  0.61%   (87 samples)
carleryd commented 4 years ago

@Mistyputt I just ran into the same issue where I want to know which position in a row an element was clicked. The row is created using map function. I would like to do the following

L.map((x, i) => <Poster content={x} position={i} />, contentL)

I guess a workaround for now can be to use indexOf

paldepind commented 4 years ago

Thanks for opening the issue and providing feedback. I definitely agree that it would be nice to offer an easy way to map with an index.

I think your mapWithIndex implementation looks great @Mistyputt. Would you like to add it in a PR? 😄

Mistyputt commented 4 years ago

Absolutely!

I did work around a few issues building the project when trying out that implementation and would feel better if I had a "clean" project when submitting the PR. After cloning I ran npm install, then npm run build. That failed with errors on ts-toolbelt and devtoolsFormatters:

❯ npm run build

> list@2.0.19 build /Users/thomas/git/github/funkia/list3
> npm run build-es6; npm run build-cmjs

> list@2.0.19 build-es6 /Users/thomas/git/github/funkia/list3
> tsc -P ./tsconfig-build.json --outDir 'dist/es' --module es2015

node_modules/@types/ramda/index.d.ts:546:28 - error TS2307: Cannot find module 'ts-toolbelt'.

546 import { A, F, T, O } from "ts-toolbelt";
                               ~~~~~~~~~~~~~

src/devtools.ts:15:8 - error TS2339: Property 'devtoolsFormatters' does not exist on type '(Window & typeof globalThis) | Global'.
  Property 'devtoolsFormatters' does not exist on type 'Window & typeof globalThis'.

15 if (gw.devtoolsFormatters === undefined) {
          ~~~~~~~~~~~~~~~~~~

src/devtools.ts:16:6 - error TS2339: Property 'devtoolsFormatters' does not exist on type '(Window & typeof globalThis) | Global'.
  Property 'devtoolsFormatters' does not exist on type 'Window & typeof globalThis'.

16   gw.devtoolsFormatters = [];
        ~~~~~~~~~~~~~~~~~~

src/devtools.ts:26:4 - error TS2339: Property 'devtoolsFormatters' does not exist on type '(Window & typeof globalThis) | Global'.
  Property 'devtoolsFormatters' does not exist on type 'Window & typeof globalThis'.

26 gw.devtoolsFormatters.push({
      ~~~~~~~~~~~~~~~~~~

Found 4 errors.

npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! list@2.0.19 build-es6: `tsc -P ./tsconfig-build.json --outDir 'dist/es' --module es2015`
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the list@2.0.19 build-es6 script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/thomas/.npm/_logs/2020-04-30T21_15_23_326Z-debug.log

> list@2.0.19 build-cmjs /Users/thomas/git/github/funkia/list3
> tsc -P ./tsconfig-build.json

src/devtools.ts:15:8 - error TS2339: Property 'devtoolsFormatters' does not exist on type '(Window & typeof globalThis) | Global'.
  Property 'devtoolsFormatters' does not exist on type 'Window & typeof globalThis'.

15 if (gw.devtoolsFormatters === undefined) {
          ~~~~~~~~~~~~~~~~~~

src/devtools.ts:16:6 - error TS2339: Property 'devtoolsFormatters' does not exist on type '(Window & typeof globalThis) | Global'.
  Property 'devtoolsFormatters' does not exist on type 'Window & typeof globalThis'.

16   gw.devtoolsFormatters = [];
        ~~~~~~~~~~~~~~~~~~

src/devtools.ts:26:4 - error TS2339: Property 'devtoolsFormatters' does not exist on type '(Window & typeof globalThis) | Global'.
  Property 'devtoolsFormatters' does not exist on type 'Window & typeof globalThis'.

26 gw.devtoolsFormatters.push({
      ~~~~~~~~~~~~~~~~~~

As no one else has reported this I feel I'm missing something obvious. When I played around with the mapWithIndex implementation I just commented out src/devtools.ts and ran the build-cmjs only, not the es6 that additionally failed on ts-toolbelt.

I built with node v10.19.0. Any pointers?