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

Add typescript supports #140

Closed
mizunashi-mana opened this issue May 11, 2016 · 26 comments
Closed

Add typescript supports #140

mizunashi-mana opened this issue May 11, 2016 · 26 comments

Comments

@mizunashi-mana
Copy link

Suggestions

  • Define TypeScript definition
  • Add them on DefinitelyTyped.
  • Fix existing types supporting fantasy land.

Notes

TypeScript can add types without any binding codes. So, we can support TypeScript just to write type definition codes.
TypeScript manage definitions on main definitions repository, DefinitelyTyped.
Already, some definitions of library supporting fantasy land (parsimmon, tsmonad, etc.) were added.

However, the types of each library were defined by some contributors and has some differences...
I would like you to define TypeScript type signatures of fantasy land for TypeScript and fantasy land user.

That has good influences for us.
Thanks!

@joneshf
Copy link
Member

joneshf commented May 11, 2016

If you can type Functor in TypeScript, then go for it. Last I checked, you couldn't.

@mizunashi-mana
Copy link
Author

mizunashi-mana commented May 11, 2016

@joneshf
Actually, I usually use types of TsMonad now.
And, we can use these.

Functor:
https://github.com/cbowdon/TsMonad/blob/master/monad.ts#L97

@mizunashi-mana
Copy link
Author

mizunashi-mana commented May 11, 2016

Oh... Apply type cannot write in TypeScript...

This is illegal typescript code, but I wanted.

interface Apply<(t: T) => U> {
   ap(u: U): U; 
}

interface Apply<T> {
}

However, Apply type is very strange for me. I think all Apply data must have ap method. Functor type ( map :: Functor f => f a -> (a -> b) -> f b ) is good for me.
If we will support typescript for fanrasy-land, we change Apply type to:

a.ap(f); // a :: A<T>, f :: A<(T => U)>, a.ap(f) :: A<U>

or

A.ap(a, f); // a :: A<T>, f :: A<T => U>, A.ap(a, f) :: A<U>

In this cases, the typescript types are:

interface Apply<T> {
  ap<U>(f: Apply<(t: T) => U>): U;
}

or

interface Apply<T> {
  ap(a: T, f: Apply<(t: T) => U>): Apply<U>;
}

interface ApplyStatic<T, U> {
  new(...args: any[]): Apply<any>;
  ap(a: Apply<T>, f: Apply<(t: T) => U>): Apply<U>;
}

Maybe and Either of TsMonad are not supported Apply and Applicative. The reason may be this problem...

@SimonRichardson
Copy link
Member

How do you do a natural transformation in typescript as well or high order
types. I think you'll hit road blocks a lot and I wonder if it would be
worth it, esp. when you work with free monads and coyoneda.

On Wed, 11 May 2016, 20:31 Mizunashi Mana, notifications@github.com wrote:

Oh... Apply type cannot write in TypeScript...

This is illegal typescript code, but I wanted.

interface Apply<(t: T) => U> {
ap(u: U): U;
}
interface Apply {
}

However, Apply type is very strange for me. I think all Apply data must
have ap method.
If we will support typescript for fanrasy-land, we change Apply type to:

a.ap(f): // a :: A, f :: A<(T => U)>, a.ap(f) :: A

or

A.ap(a, f); // a :: A, f :: A<T => U>, A.ap(a, f) :: A

In this cases, the typescript types are:

interface Apply {
ap(f: Apply<(t: T) => U>): U;
}

or

interface Apply {
ap(a: T, f: Apply<(t: T) => U>): Apply;
}
interface ApplyStatic<T, U> {
new(...args: any[]): Apply;
ap(a: Apply, f: Apply<(t: T) => U>): Apply;
}

Maybe and Either of TsMonad are not supported Apply and Applicative. The
reason may be this problem...


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#140 (comment)

@mizunashi-mana
Copy link
Author

I understood we cannot natural transforming in typescript. TypeScript has some problems on type system. Yoneda type can write on TypeScript because this type is only running. But, CoYoneda cannot.
So, we cannot provide full support for TypeScript.

However, I think we can provide main support (Monad/Monoid/Traversal).
This may be also helpful for JavaScripter.

Notes

In TypeScript:

const m = (Functor<A>)Identity(new A());
m.equals(a => Identity(new A()); // this will be compile error

But, this will be ok:

interface Functor<T> {
  map<U>(f: (t: T) => U): Functor<U>;
}

interface Eq<T> {
  equals(t: T): boolean;
}

class A<T> implements Functor<T>, Eq<A<T>> {
  map<U>(f: (t: T) => U): A<U> { return new A(); }
  equals(a: A<T>): boolean { return true; }
}

new A().map(a => a).equals(new A());

So, that cases, we cannot:

function fmap<T, U>(f: (t: T) => U): (g: Functor<T>) => Functor<U> {
  return a => a.map(f);
}

fmap(a => a)(new A()).equals(new A()); // compile error

In this cases, TypeScript user force to cast types...

@SimonRichardson
Copy link
Member

You've just shown why the typescript support is pointless from my point of view, by all the notes and changes you've suggested that are required.

I just think the typescript system is too restrictive, by not being free enough (oxymoron considering we're talking about JavaScript).

👎 from me

@mizunashi-mana
Copy link
Author

Ok, I considered deeply, Apply design is well and to change this design effects some wrong for javascript fantasy land user. And, typescript support is not well for this project.

I'm sorry for taking your time 🙇 .

@SimonRichardson
Copy link
Member

I'm sorry for taking your time

Please note, the issue wasn't about your proposal, but about the fact that TypeScript is limited by its very nature. Therefore the gains/rewards that we get from it are limited and may even restrict the future additions to the specification.

Having said all of this, there isn't anything stopping you from making the repo hosted somewhere else though, if you do get a benefit from having the TypeScript definitions.

@mizunashi-mana
Copy link
Author

@SimonRichardson
I see.
However, I cannot define Apply type on TypeScript. Adding TypeScript definitions limit APIs for libraries supporting fantasy-land, and I think that disadvantages are greater than advantages providing standardization.

So, I stop declaring TypeScript types.
Thanks.

@joneshf
Copy link
Member

joneshf commented May 12, 2016

@joneshf
Actually, I usually use types of TsMonad now.
And, we can use these.

Functor:
https://github.com/cbowdon/TsMonad/blob/master/monad.ts#L97

Tell me if I'm wrong here (since I don't know typescript that in-depth), but that doesn't seem to describe Functor properly. It allows the implementer to specify which values they want to work with. Functors don't get to make that choice. Given that interface could we not change this line: https://github.com/cbowdon/TsMonad/blob/f359e2d772a7d6f5d21eba3ba4db85be5a1bd83b/maybe.ts#L54 to the following (and the subsequence implementation) and still have everything typecheck?

export class Maybe<T> implements Monad<T>, Functor<boolean>, Eq<Maybe<T>> {

@mizunashi-mana
Copy link
Author

@joneshf
I understood your talking about. TsMonad's interface is wrong. Sorry.

However,

class Maybe<T> {
  fmap<U>(f: (t: T) => U): Maybe<U> { ... }
}

this one can be Functor, cannot? The TsMonad Functor interface can be a helper for declare type.
That will be compile error.

class Maybe<T> implements Functor<boolean> {
  fmap<U>(f: (t: T) => U): Maybe<U> { ... }
  /* fmap(f: (t: boolean) => boolean): Maybe<T> { ... } // can define */
  /* fmap<U>(f: (t: boolean) => U): Maybe<U> { ... } // also can define... */
}

Actually, I'm usual using PureScript, so, not deep TypeScript user. So, maybe, there are some solutions I don't know.

@joneshf
Copy link
Member

joneshf commented May 13, 2016

Sorry, I wasn't very clear.

The last example is what I meant.

class Maybe<T> implements Functor<boolean> {
  fmap<U>(f: (t: boolean) => U): Maybe<U> { ... }
}

That's not a Functor.

But you're right, the first example is a Functor. This one:

class Maybe<T> {
  fmap<U>(f: (t: T) => U): Maybe<U> { ... }
}

You can define them inline to a class, you just can't abstract out the interface with typescript.

@mizunashi-mana
Copy link
Author

@joneshf
I see. That is exactly right. Such code has problems.

function void<T extends Functor<U>, U, R extends Functor<{}>>(fa: T): R {
  return fa.fmap(a => ({}));
}

That type signature passed that:

function void<T extends Functor<U>, U, R extends Functor<{}>>(fa: T): R {
  return Maybe.Just({});
}

I understood that TsMonad's Functor interface is wrong for that cases.
Thanks.

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Nov 30, 2016

I tried to make an attempt at this (not having looked at TsMonad) with my limited understanding of these types, and within the limitations of TS interfaces (no static methods, no multiple extending).
I must say I think TS has been making progress; if it isn't there yet, hopefully it'll still get there.

    interface Setoid<T> {
        equals(b: Setoid<T>): boolean;
    }

    interface Semigroup<T> {
        concat(b: Semigroup<T>): Semigroup<T>;
    }

    interface Monoid<T> extends Semigroup<T> {
        /* static */ empty<T>(): Monoid<T>;
    }

    interface Functor<T> {
        map<U>(fn: (t: T) => U): Functor<U>;
    }

    interface Apply<T> extends Functor<T> {
        apply<U>(fn: Apply<(t: T) => U>): Apply<U>;
    }

    interface Applicative<T> extends Apply<T> {
        /* static */ of<U>(a: U): Applicative<U>;
    }

    interface Alt<T> extends Functor<T> {
        alt(b: T): Alt<T>;
    }

    interface Plus<T> extends Alt<T> {
        /* static */ zero<T>(): Plus<T>;
    }

    interface Alternative<T> extends Plus<T>, Applicative<T> {
    }

    interface Foldable<T> {
        reduce<U>(fn: (u: U, t: T) => U, u: U): U;
    }

    interface Traversable<T> extends Functor<T>, Foldable<T> {
        traverse<U, V>(fn: (t: T) => Applicative<U>, of: (v: V) => Applicative<V>): Applicative<Traversable<U>>;
    }

    interface Chain<T> extends Apply<T> {
        chain<U>(fn: (t: T) => Chain<U>): Chain<U>;
    }

    interface ChainRec<T> extends Chain<T> {
        /* static */ chainRec<A,B,C>(f: (next: (a: A) => C, done: (b: B) => C, value: A) => ChainRec<C>, i: A): ChainRec<B>;
    }

    interface Monad<T> extends Applicative<T>, Chain<T> {
    }

    interface Extend<T> {
        extend<U>(f: (v: Extend<T>) => U): Extend<U>;
    }

    interface Comonad<T> extends Functor<T>, Extend<T> {
        extract<U>(): U; // 'same U as in extend's f -- how to bind?
    }

    interface Bifunctor<T,U> extends Functor<T> /*, Functor<U>*/ {
        bimap<B,D>(f: (v: T) => B, g: (v: U) => D): Bifunctor<B,D>;
    }

    interface Profunctor<T,U> extends Functor<T> /*, Functor<U>*/ {
        promap<B,D>(f: (v: T) => B, g: (v: U) => D): Profunctor<B,D>;
    }

I've likely made mistakes, but I hope this might be useful.

@paldepind
Copy link

paldepind commented Nov 30, 2016

@tycho01 The problem with these is that some of them actually don't work. I've tried similar interface definitions and found that they're not as good as they seem.

Consider your Functor interface:

interface Functor<T> {
    map<U>(fn: (t: T) => U): Functor<U>;
}

This says that map can return any functor. This means that after calling map on a Functor the only thing you know about the return value is that it is some Functor. I.e. you only know that it has a map method. But that is useless—so you'll have to cast it.

We can't write a proper Functor interface since TypeScript lacks higher kinded types. I have run into similair problems in my library Jabz. The best solution I've come of with is to create a map function of the following type: map(f: (t: T) => U, Functor<T>): any. Where Functor is similar to your definition. The any prevents it from being typesafe. But I can find no better solution since the alternative is having a map that requires casting after being called. That is less convenient and just a unsafe.

@KiaraGrouwstra
Copy link
Contributor

@paldepind: If I understand correctly, the point here is that even if you have a Maybe implement such an interface, the existing Functor<U> return type would not give enough information to tell you that on a Maybe it should return a Maybe, correct?
It seems that TypeScript recently made a change that feels sort of similar to this ("Use returned values from super calls as 'this'"), though I suppose it's a different context (constructors).

I tried to see if I could make a quick reproduction of the issue you were explaining about, presuming I understand it correctly. I'm trying the following:

interface Functor<T> {
    map<U>(fn: (t: T) => U): Functor<U>;
}

class MyFunctor<T> implements Functor<T> {
    constructor(public val: T) {};
    map<U>(fn: (t: T) => U): MyFunctor<U> {
        return new MyFunctor(fn(this.val));
    }
}

let strFtor = new MyFunctor('hi');
let numFtor = strFtor.map(s => s.length);
// ^ inferred to be a MyFunctor<number>, as expected

Would you mind explaining a bit further?

@SimonRichardson
Copy link
Member

SimonRichardson commented Nov 30, 2016

There could be a possibility in using a different encoding for HKT as seen in Java or similar. I did try and encode this sort of thing into Haxe, but I ended up having to cast at some point because of the issues with the type system.

I've not used typescript so don't know to what extent any of this is possible.

@SimonRichardson
Copy link
Member

@tycho01 how does it work for chain, considering you don't return new MyFunctor directly, but from fn.

@KiaraGrouwstra
Copy link
Contributor

@SimonRichardson: Thank you; with a similar attempt at Chain I managed to reproduce your reported issues. I attempted a similar method signature of chain<U>(fn: (t: T) => MyChain<U>): MyChain<U>, but on an attempt to implement this, things break down; fn's return type of MyChain<U> is quickly found to be incompatible with the asked return type of Chain<U>, stating every possible difference between them (essentially things the MyChain implementation has extra). I'd hoped TypeScript defaulted to covariance here and would be automatically able to deal with it, but it appears that isn't the case.
I'm not super familiar with how workarounds might work here, but it appears nominal typing is on the TS roadmap. :)

@mizunashi-mana, I think the problem in the interface Apply<(t: T) => U> notation was that you should put generics in there by name, e.g. interface Apply<T, U, V extends (t: T) => U>. That said, my Apply implementation ended up a bit simpler there. :P

@KiaraGrouwstra
Copy link
Contributor

@SimonRichardson: I guess you were already on the right path here, but I've been corrected in that thread on needing support for HKT rather than nominal typing.

@rjmk
Copy link
Contributor

rjmk commented Dec 2, 2016

@tycho01 It's actually support for higher-kinded polymorphism. Typescript already has HKTs (i.e. generics)

@masaeedu
Copy link

masaeedu commented Jan 4, 2017

@rjmk Typescript doesn't have HKTs, i.e. you can't have a type parametrized by a parametrized type. There is an open issue tracking this.

@rjmk
Copy link
Contributor

rjmk commented Jan 4, 2017

@masaeedu I was drawing a distinction between type constructors/parameterised types (functions from types to either a type or a type constructor) vs functions from type constructors to types/type constructors. Since I made that comment, I've discovered not everyone makes the distinction with that terminology

@TJSomething
Copy link

TJSomething commented Mar 25, 2017

I think that I may have figured out how to encode Functor. I avoided the problem of overly general return types by using a string literal type tag. I avoided the lack of covariance that @tycho01 ran into by using adding value: any to the Functor interface to hold the data.

interface Functor<Tag extends string, T> {
  tag: Tag;     // A tag unique to the functor
  value: any;   // Holds the actual data. 
  fmap<U>(f: (x: T) => U): Functor<Tag, U>;
}

class ArrayWrapper<T> implements Functor<"ArrayWrapper", T> {
  tag: "ArrayWrapper";
  readonly value: Array<T>;

  constructor(elements: Array<T>) {
    this.value = elements;
  }

  fmap<U>(f: (x: T) => U): ArrayWrapper<U> {
    return new ArrayWrapper<U>(this.value.map(f));
  }
}

class Maybe<T> implements Functor<"Maybe", T> {
  tag: "Maybe";
  readonly value: T | null;

  constructor(newValue: T | null) {
    this.value = newValue;
  }

  fmap<U>(f: (x: T) => U): Maybe<U> {
    return this.value === null
      ? new Maybe<U>(null)
      : new Maybe(f(this.value));
  }
}

function f<Tag extends string, T>(x: Functor<Tag, T>) {
  return x.fmap((x: T) => true);
}

const x = new ArrayWrapper([1]);

// This typechecks.
const doesTypecheck: ArrayWrapper<boolean> = f(x)
// You can't assign functors to other functors, even when they share parameters. 
const doesNotTypecheck: Maybe<boolean> = f(x);

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Mar 25, 2017

@TJSomething: Interesting! Comments:

  • on typing value as any, I wonder if there might be a point in capturing its type in an extra generic in Functor, so the Functor type could tell just a little more about what kind of instance we'd be dealing with. I've no idea if adding this info would be deemed out of scope in the ADT definitions. Either way, you've already demonstrated this isn't needed for it to function.
  • the unfortunate part about using this for ArrayWrapper: we still wouldn't have it count the built-in types :(, though having a properly-typed Maybe already seems like a big win.
  • nitpick: that fmap, I thought that should be map, while the flat variant would be the one added by Monad?

If we would be able to properly type the ADTs (I haven't retried Chain), I think that could make for a follow-up question for fantasy-land: might it make sense to at that point make interface typings (TS and/or Flow) part of this specification/library?

I'd be inclined to think this could be beneficial for the standardization process: if you type your library implementing some ADT instances, and it type-checks with the interfaces, you're probably in proper compliance with the spec.

@gcanti
Copy link

gcanti commented Jun 29, 2017

@TJSomething I used the same technique in fp-ts, though after the 2.4.1 release I'm afraid I have to rewrite the lib from the ground up gcanti/fp-ts#138 (comment)

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

No branches or pull requests

9 participants