Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BREAKING] Remove IteratorSequence. Do not attempt to detect iterators. #1589

Merged
merged 1 commit into from
Sep 18, 2018

Conversation

conartist6
Copy link
Contributor

Fixes #1456

@Brantron
Copy link

Out of curiosity, what is the speed/perf impact of this change?

@conartist6
Copy link
Contributor Author

There could be a [very] small win since sequences are sometimes constructed internally as part of other implementations, but my motivation for this change is correctness not perf.

@leebyron leebyron changed the title Remove IteratorSequence. Do not attempt to detect iterators. [BREAKING] Remove IteratorSequence. Do not attempt to detect iterators. Sep 18, 2018
@leebyron
Copy link
Collaborator

This is the right thing to do. Thank you for your investigation and work.

All built in iterables define [Symbol.iterator]() { return this } and most libraries do as well, so in practice this should be a very minor breaking change.

@leebyron leebyron merged commit f4e0f50 into immutable-js:master Sep 18, 2018
@ShawnTalbert
Copy link

I've relied on Seq taking an iterator instance for years. It's a big breaking change for me.

I still need to support an ES5 runtime environment.
Is my only option to stay back on the 3.8 release of immutablejs or have to bloat my solution with a polyfill, if that's even workable in this environment?

@leebyron
Copy link
Collaborator

Unfortunately yes, you'll need to ensure whatever iterators provided are also Iterable. Immutable determines that via finding a function at Symbol.iterator. While you may need to polyfill that, note that Symbol.iterator doesn't actually have to be a symbol, so you can still target ES5 environments.

@conartist6
Copy link
Contributor Author

conartist6 commented Oct 24, 2021

@ShawnTalbert The simplest fix is this:

const iteratorSymbol = require('core-js-pure/es/symbol/iterator');

Seq({ [iteratorSymbol]: () => yourIterator });

Sorry for the inconvenience to you, but from our perspective it's much safer this way.

@conartist6
Copy link
Contributor Author

That polyfill isn't free but it doesn't change the global namespace which helps if you don't own the environment, and it won't introduce any more bloat than is absolutely necessary when you use a tree-shaking bundler like rollup or webpack.

@bdurrer
Copy link
Contributor

bdurrer commented Oct 24, 2021

This should be mentioned in the changelog.md, I guess?

@ShawnTalbert
Copy link

Thanks folks, I'll try the ideas above. In my case it's end users that call Seq ( myiterator ) in many files across many clients. I'm not sure if I can assign global variables at all in this ES5 environment even if I wanted to.

@conartist6
Copy link
Contributor Author

@ShawnTalbert That sounds like a use case I'd be interested to hear about. In addition to being tangentially involved in this project I maintain iter-tools, so I'm always thinking about these sorts of things. Our decision takes a cue from the way the language itself does things: it doesn't define any iterators that are not also iterables. What it does is:

const someIterator = {
  next(){
    /*...*/
  },
  [Symbol.iterator]() { return this };
};

If you can do that where the iterators are defined then you'll no longer have changes to make at every Seq call site. Do you have the option to redefine the iterators passed to you in such a fashion?

@conartist6
Copy link
Contributor Author

conartist6 commented Oct 25, 2021

@ShawnTalbert Hey sorry I'm behind on this as I haven't poked into the code in a while. We're leading you on a little here -- you don't actually need to define Symbol.iterator at all. We also accept the backup '@@iterator', as you can see here.

So putting it all together, you want to change the definition of your iterators to:

const someIterator = {
  next: function() {
    /*...*/
  },
  "@@iterator": function () { return this };
};

Hope this helps!

@ShawnTalbert
Copy link

Heh, thanks @conartist6 - indeed just a few minutes ago I ended up adding the same to my iterator class and Immutable4 seems happy.

/**
    * Fake Symbol.iterator to make this an `Iterable` to satisfy ImmutableJS 4.0.0 requirements on Seq()
    * Since we still need NFT to work in ES5 (SS2.0) using `@@iterator` rather than `Symbol.iterator` (which does
    * not exist in ES5).
    */
   ['@@iterator'](){ return this }

@ShawnTalbert
Copy link

ShawnTalbert commented Oct 25, 2021

Actually, there perhaps remains a [TypeScript] type issue? Here are all the overloads for Seq in the .d.ts file:

 function Seq<S extends Seq<unknown, unknown>>(seq: S): S;
  function Seq<K, V>(collection: Collection.Keyed<K, V>): Seq.Keyed<K, V>;
  function Seq<T>(collection: Collection.Set<T>): Seq.Set<T>;
  function Seq<T>(
    collection: Collection.Indexed<T> | Iterable<T> | ArrayLike<T>
  ): Seq.Indexed<T>;
  function Seq<V>(obj: { [key: string]: V }): Seq.Keyed<string, V>;
  function Seq<K = unknown, V = unknown>(): Seq<K, V>;

While this seemed to work at runtime, my 'fake' ES5 Iterable does not seem to fit any of the overloads above, so I'm getting a compile-time error. Do we need to add an Itereable-like sort of type to one of the overloads or am I missing something? Iterable<T> from lib.es2015.iterable.d.ts relies on real Symbol.iterator for the function name (i.e. no '@@iterator' backup.

@conartist6
Copy link
Contributor Author

Oof. You might be the first person to try that. Why not do something evil? You were able to define that @@iterator string in one place it sounded like, so just cast it to typeof Symbol.iterator.

@ShawnTalbert
Copy link

I realize this is asking for support on a "legacy" pre-Symbol version of JS but if we're still (thankfully) trying to support ES5 for Seq, isn't it reasonable that the Seq() typings would reflect that?

For example, even if evil casting is possible, it means adding some sort of ES2015.Symbol to my tsconfig.json - which is lying as the real (ES5) environment doesn't actually have Symbols. Or said another way, the Seq overload that accepts an Iterable<T> is more restrictive than Seq() actually is since it also supports the @@iterator fallback.

Would it be acceptable to add an IterableLike alongside Iterable on the relevant Seq() overload to identify ES5/faux-symbol support?

type IterableLike<T> = { ['@@iterator']() : Iterator<T> }

I wasn't able to get any casting approaches to work (per your previous comment) but will keep trying in the meantime. Adding a type like above to Seq() would certainly make me feel better as a user of the lib.

@conartist6
Copy link
Contributor Author

Your syntax is a little off there. I couldn't say how well it would work or how much work it would be without trying. You'd be the ideal person to try, since you have the real test case. If you can modify the typedefs such that they work correctly for you (and don't break things for anyone else), I'd imagine we could get the changes merged.

@conartist6
Copy link
Contributor Author

The easiest way to do that in my experience is just to experiment with changing the real installed typedefs in node_modules/@types/immutable. Make sure you copy your changes out of that file regularly though in case the package manager should overwrite them.

ShawnTalbert pushed a commit to ShawnTalbert/immutable-js that referenced this pull request Oct 25, 2021
…property named `@@iterator` (e.g. for environments pre-ES2015 where `Symbol.iterator` does not natively exist). Related discussion on pull request immutable-js#1589
@leebyron
Copy link
Collaborator

This is actually a benefit of Typescript exposing these native feature types as separate importable libraries and why Immutable doesn't directly import the ES2015.Symbol or ES2015.Iterable libraries. You can import or write your own.

The ES2015 version Typescript provides is also not all that complicated, take a look https://github.com/microsoft/TypeScript/blob/main/lib/lib.es2015.iterable.d.ts

Here's the most relevant parts:

/// <reference lib="es2015.symbol" />

interface SymbolConstructor {
    /**
     * A method that returns the default iterator for an object. Called by the semantics of the
     * for-of statement.
     */
    readonly iterator: unique symbol;
}

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

interface Array<T> {
    /** Iterator */
    [Symbol.iterator](): IterableIterator<T>;
}

interface ReadonlyArray<T> {
    /** Iterator of values in the array. */
    [Symbol.iterator](): IterableIterator<T>;
}

Since you only care about providing values and not consuming them, and Immutable only cares about the yield values you'd need even less for your own version for ES5 faux iterator support:

interface Iterator<T> {
  next(): { done: boolean, value: T };
}

interface Iterable<T> {
  ['@@iterable'](): Iterator<T>;
}

interface IterableIterator<T> extends Iterator<T> {
  ['@@iterable'](): IterableIterator<T>;
}

interface Array<T> {
  ['@@iterable'](): IterableIterator<T>;
}

interface ReadonlyArray<T> {
  ['@@iterable'](): IterableIterator<T>;
}

@ShawnTalbert
Copy link

ShawnTalbert commented Oct 29, 2021

I commented more on the other PR, but the specific declarations above are an ImmutabjeJS-specific definition.

It's not 'my' version of a faux iterator, but rather ImmutableJS's required definition to be a faux iterator.

Like any other types defined by a library, I'd typically expect the declarations to come from the library. What if Immutable decides to change these faux iterator definitions down the road? I would expect a compile-time error via updated definitions coming from that newer version of the lib.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants