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

Use Symbol.species? #156

Closed
SimonMeskens opened this issue Jul 15, 2017 · 10 comments
Closed

Use Symbol.species? #156

SimonMeskens opened this issue Jul 15, 2017 · 10 comments

Comments

@SimonMeskens
Copy link
Collaborator

A more canonical way to do branding in modern JavaScript is Symbol.species. Javascript only defines Symbol.species when it appears as a static in a class (or as a property on a constructor function, which is the same thing). Putting it on the instance is not defined, but allowed. Basically, this is valid JavaScript code:

type Species<U, A> = {
    "fp-ts/kind": U
    "fp-ts/type": A
}

interface HKT<U, A> {
    [Symbol.species]: Species<U, A>
}

const URI = "Test";
type URI = "Test";

class Test<A> implements HKT<URI, A> {
    [Symbol.species]: Species<URI, A>
}

No more underscores (uses namespacing with "fp-ts/" instead) and no polluting the object. Advantage is that symbols are not enumerable at runtime, and they don't show up in keyof at compile time, while still providing their function in constraints. the namespacing with "fp-ts" is optional of course, but I expect other libraries will start using Symbol.species in the future and as fantasy-land indicated, "namespace/" is kind of the canonical way to avoid collisions.

@alexandru
Copy link

I'm not familiar with Symbol or with how Symbol.species works.

Is there a good document anywhere on Symbol.species that I can read?

@SimonMeskens
Copy link
Collaborator Author

The mozilla doc is pretty good and links to the ECMA spec: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/species

Basically, the trick here is that Symbol.species is only defined as a static (property on the constructor), so we are free to use it for other purposes, but you could also attach namespaced properties on the static species if you so wish I think.

@sledorze
Copy link
Collaborator

Not sure how the Symbol.species may be used but we must consider its adoption (the IE story does not look that great).

@SimonMeskens
Copy link
Collaborator Author

Hmm, good point. There's a polyfill for IE, but maybe we should find a different spec that doesn't rely on Symbol.

As far as I can tell, Microsoft doesn't plan on supporting a bunch of new technologies with IE. I've already decided not to support Safari for any of my projects for similar reasons (people just get a page telling them to get a real browser), but I realize this is not an option for everyone.

@alexandru
Copy link

If Symbol.species is not currently supported on Microsoft's browser, that's a conversation killer.

It's one thing for an application to decide to drop support for Microsoft stuff, quite another for a library, especially since developers might end up finding out about it after they've deployed it in a production environment and it breaks for users, because honestly, we aren't reading the freaking manual most of the time 😀

@strax
Copy link

strax commented Dec 31, 2017

Given that TypeScript will include support for indexing via symbols (microsoft/TypeScript#15473), a viable (and less spec-violating) approach would be to define a custom symbol type that would then be used to brand the type application.

const type = Symbol("type");

type _ = any;

interface TypeTag<U, A> {
  readonly kind: U;
  readonly type: A;
}

interface App<U, A> {
  [type]: TypeTag<U, A>
}

abstract class Functor<F> {
  abstract map<A, B>(fa: App<F, A>, f: (a: A) => B): App<F, B>;
}

// Using _ as any makes clear our intentions about App:
// App<Container<_>, T> reads as "apply T to the partial application in Container<_>"
class Container<T> implements App<Container<_>, T> {
  [type]: TypeTag<Container<_>, T>

  constructor(public readonly get: T) {
  }
}

// In Scala this would be `extends Functor[Box[_]]` to denote higher-kind application
class ContainerFunctor extends Functor<Container<_>> {
  map<A, B>(container: App<Container<_>, A>, f: (a: A) => B): Container<B> {
    // Here we'll cast the type application to a concrete U<A> type
    const a = (container as Container<A>).get;
    return new Container(f(a));
  }
}

const f = new ContainerFunctor();
f.map(new Container(1), a => a);

This would still have the advantage of preventing access to internal properties, tooling would ignore them and, symbols being unique, would prevent naming or semantic conflicts with other libraries and conventions. Symbols are also well-supported in all modern browsers if we're not relying on a certain builtin symbol like species mentioned above.

@SimonMeskens
Copy link
Collaborator Author

Good idea!

Would we be better off defining a global system with a namespaced name, so we don't need a dependency library?

const type = Symbol.for("fantasy-land/type");

Not sure which namespace would be best, so I just picked fantasy-land, because it's popular. That one has the advantage of not needing a dependency to provide the symbol, as well as being unique enough.

@gcanti
Copy link
Owner

gcanti commented Feb 13, 2018

HKT encoding is now frozen (v1.0.0)

@gcanti gcanti closed this as completed Feb 13, 2018
@rui-atg
Copy link

rui-atg commented Mar 12, 2020

Why not use Symbol as tags instead of string, in Option for instance?

const NoneSym = Symbol('NoneSym')
export interface None {
  readonly _tag: NoneSym
}

Comparisons would be faster, i.e. in line 126 would be return fa._tag === NoneSym

@raveclassic
Copy link
Collaborator

@rui-atg Perhaps because TypeScript cannot do type narrowing based on symbols.

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

No branches or pull requests

7 participants