0
\$\begingroup\$

I've published a new TypeScript library currently in beta, and was hoping to get feedback on certain aspects of it, particularly implementation details for improving the performance of deferred iteration in some specific methods.

The library is called enumerable-ts and is available on npm. I don't anticipate that it will be particularly impressive in any benchmarks but I'd at least like to refine some of the method implementations so that the asymptotic performance is better if possible, and also readability.

Below are some of the methods I'd like to improve:

Enumerable.prototype.distinct() (modified but not yet committed)

distinct (this: IEnumerable<TSource>, equality: EqualityFunction<TSource> = equalityFn): Enumerable<TSource> {
  return new Enumerable(function * (this: IEnumerable<TSource>, equality: EqualityFunction<TSource>) {
    const set = equality === equalityFn ? new Set<TSource>() : [] as TSource[]
    const unique = (value: TSource) => {
      if (set.contains(value, equality)) {
        return false
      }

      if (set instanceof Set) set.add(value)
      else set.push(value)

      return true
    }

    yield * this.where(unique)
  }.bind(this, equality), Enumerable.isEnumerable(this) ? this.compare : undefined)
}

The problem with distinct() was that it didn't seem very DRY, given that the implementation was essentially twice as long since it basically had analogous approaches for the general case and the optimized case when you can rely on the comparison behavior of the builtin Set, i.e. when the equality parameter uses the default behavior. Is this an acceptable way of making the method more succinct or is there an even better way?

Enumerable.prototype.groupJoin()

groupJoin<TInner, TKey> (this: IEnumerable<TSource>, inner: IEnumerable<TInner>, selectOuterKey: IndexedSelectFunction<TSource, TKey>, selectInnerKey: IndexedSelectFunction<TInner, TKey>): Enumerable<IGrouping<TSource, TInner>>
groupJoin<TInner, TKey, TResult> (this: IEnumerable<TSource>, inner: IEnumerable<TInner>, selectOuterKey: IndexedSelectFunction<TSource, TKey>, selectInnerKey: IndexedSelectFunction<TInner, TKey>, selectResult: ResultFunction<TSource, IEnumerable<TInner>, TResult>): Enumerable<TResult>
groupJoin<TInner, TKey, TResult> (this: IEnumerable<TSource>, inner: IEnumerable<TInner>, selectOuterKey: IndexedSelectFunction<TSource, TKey>, selectInnerKey: IndexedSelectFunction<TInner, TKey>, selectResult?: ResultFunction<TSource, IEnumerable<TInner>, TResult>): Enumerable<IGrouping<TSource, TInner>> | Enumerable<TResult> {
  const outerEntries = this.select(selectEntry(selectOuterKey, selectFn as SelectFunction<TSource, TSource>))
  const innerGroups = inner.groupBy(selectInnerKey)
  const groups = outerEntries.select(
    ([outerKey, outer]) => new Grouping<TSource, TInner>(function * (innerGroups: IEnumerable<IGrouping<TKey, TInner>>, outerKey: TKey) {
      yield * innerGroups.firstOrDefault(
        new Grouping(generatorFn as () => IterableIterator<TInner>, outerKey),
        ({ key }) => equalityFn(key, outerKey)
      )
    }.bind(innerGroups, outerKey), outer) as IGrouping<TSource, TInner>
  )

  return selectResult === undefined
    ? groups
    : groups.select((group, index) => selectResult(group.key, group.asEnumerable(), index))
}

While I'm fairly certain this implementation matches the expected behavior ported from Enumerable.GroupJoin() in .NET, I find the logic to be quite unfollow-able without several minutes spent studying the code. I'd like to improve this.

Enumerable.prototype.memoize()

memoize (this: IEnumerable<TSource>): Enumerable<TSource> {
  const cache: TSource[] = []
  const iterator = this[Symbol.iterator]()

  return new Enumerable(function * (this: IEnumerable<TSource>, cache: TSource[], iterator: IterableIterator<TSource>) {
    for (let index = 0; ; index++) {
      if (index === cache.length) {
        const { value, done } = iterator.next()

        if (done) return

        cache[index] = value
      }

      yield cache[index]
    }
  }.bind(this, cache, iterator), Enumerable.isEnumerable(this) ? this.compare : undefined)
}

While I'm actually fairly proud of the simplicity of this implementation, I'm curious if this is actually the expected behavior. Do people expect multiple calls to memoize() with the same target IEnumerable to produce the same sequence? Or is it just multiple references to a return value of memoize() should always produce the same sequence (as is currently implemented)? It's based on the implementation from MoreLINQ's Memoize().

Enumerable.prototype.skip()

skip (this: IEnumerable<TSource>, count: number): Enumerable<TSource> {
  if (Array.isArray(this)) {
    return new Enumerable(function * (this: TSource[], count: number) {
      for (let index = count; index < this.length; index++) {
        yield this[index]
      }
    }.bind(this, count))
  }

  return this.where((_, index) => index >= count)
}

The obvious problem with this one, is the lack of introspection available on the Enumerable class (and Map and Set) to be able to seek to specific indices of the iteration. Is there a way of making this introspection possible on the Enumerable class without too much bloat? This would actually improve the performance of a whole class of methods on Enumerable, not just skip().

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.