5

I've been experimenting with TypeScript lately, and I'm trying to implement some basic monads. I've already got a reasonably functioning Maybe (with the same methods of my Either below, at least), but Either is eluding me for type-related reasons I don't quite understand.

My typing strategy for higher kinded types is borrowed from this article: https://medium.com/@gcanti/higher-kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe. I know there are libraries that already have all these monads and other FP goodies, but implementing them like this is my way of trying to learn TypeScript more deeply.

declare module './HKT' {
  interface TypeIDtoHKT2<E, A> {
    Either: Either<E, A>;
  }
}

export const TypeID = 'Either';
export type TypeID = typeof TypeID;

export class Left<E> {
  readonly _F!: TypeID;
  readonly _A!: E;
  readonly _tag: 'Left' = 'Left';
  constructor(readonly left: E) {}
  static of<E, A>(e: E): Either<E, A> {
    return new Left(e);
  }
  map<A, B>(f: (a: A) => B): Either<E, B> {
    return this;
  }
  chain<A, B>(f: (a: A) => Either<A, B>): Either<E, B> {
    return this;
  }
  ap<A, B>(f: Either<A, (a: A) => B>): Either<E, B> {
    return this;
  }
}

export class Right<A> {
  readonly _F!: TypeID;
  readonly _A!: A;
  readonly _tag: 'Right' = 'Right';
  constructor(readonly right: A) {}
  static of<A, E>(a: A): Either<E, A> {
    return new Right(a);
  }
  map<E, B>(f: (a: A) => B): Either<E, B> {
    return Right.of(f(this.right));
  }
  chain<E, B>(f: (a: A) => Either<E, B>): Either<E, B> {
    return f(this.right);
  }
  ap<E, B>(f: Either<E, (a: A) => B>): Either<E, B> {
    if (f instanceof Left) return f;
    return this.map(f.right);
  }
}

export type Either<E, A> = Left<E> | Right<A>;

export const left = <E, A>(e: E): Either<E, A> => {
  return new Left(e);
};

export const right = <E, A>(a: A): Either<E, A> => {
  return new Right(a);
};

When I try to run a test assertion, such as right(4).map(isEven) === true, I get the following error:

Cannot invoke an expression whose type lacks a call signature. Type '(<E, B>(f: (a: number) => B) => Either<E, B>) | (<A, B>(f: (a: A) => B) => Either<unknown, B>)' has no compatible call signatures.

I don't understand why the E type is unknown here or how I can make it known...or if that's even the right thing to be trying to do. Any guidance is appreciated.

M-N
  • 621
  • 7
  • 20
  • I tried all your example here https://repl.it/repls/ExtralargeFixedAssembly and works just fine. What version of Node and TS are you using? – developer_hatch Aug 17 '19 at 04:14
  • @DamiánRafaelLattenero I'm using Node 12.6.0 and TS 3.5.3. But more importantly, if I add the `right(4).map(isEven)` it does not compile. See https://repl.it/repls/BaggySuperficialGlueware – M-N Aug 17 '19 at 04:28
  • When I use Javascript syntax in a way it is not intended for it is called a hack. Using TS interfaces for faking higher kinded types is just that kind of misuse and I guess what you're experiencing are the consequences of relying on a hack. –  Aug 17 '19 at 08:45
  • @bob that's a fair point. I agree it is a clumsy solution. Is your suggestion that TS just isn't a good language choice here, and I should just go back to using algebraic structures in JS? Or do you have a better TS implementation suggestion for such structures? – M-N Aug 17 '19 at 14:28
  • If you really want to apply typed purely functional idioms than you should use purescript, because it is industry proofed. It's a hard step, though, especially when you come from imperative Javascript. Typescript is probably a good choice if you stick with OOP-style. –  Aug 17 '19 at 15:58

1 Answers1

2

The problem is here:

export const left = <E, A>(e: E): Either<E, A> => {
  return new Left(e);
};

export const right = <E, A>(a: A): Either<E, A> => {
  return new Right(a);
};

this will work just fince:

console.log(isEven(3))
console.log((new Right(4)).map(isEven))

this will work also:

console.log(right(3))

but when you try to do:

right(3).map(isEven)

the compiler goes crazy and can't bind the type correctly. I think it is a limitation of the language. just don't use that constans and use directly the constructor.

developer_hatch
  • 15,898
  • 3
  • 42
  • 75