Fantasy Land

Welcome to

TZ 2021 - Loïc TRUCHOT

What about incorporate monads?

ES6 Promise specification

A bit of context...

  • Promise: wrapper for asynchronous use of future value
  • Promise can flatten callback hell
  • Widely adopted since 2009, defacto standard 
  • Since 2012, TC39 want it in EcmaScript
    • Discussion takes place in 2013, about it implementation
    • Domenic Denicola is TC39 champion
  • Since 2015, official feature (ES6)
const randomTime = () => Math.random() * 100;

// imperative version, leads to uncertain results
let val = "";
setTimeout(() => val += "it begin. ", randomTime());
setTimeout(() => val += "it continue. ", randomTime());
setTimeout(() => val += "it end. ", randomTime());
setTimeout(() => console.log(val), randomTime());

Asynchronous problems

imperative approach

Console verdict: yuk!

AKA bugland

let val = "";
setTimeout(() => {
  val += "it begin. ";
  setTimeout(() => {
    val += "it continue. ";
    setTimeout(() => {
      val += "it end. ";
      setTimeout(() => {
        console.log(val),
        randomTime()
      });
    }, randomTime());
  }, randomTime());
}, randomTime());

Callbacks "solutions"

const start = (val: string, callback: Function) => {
    setTimeout(() => callback(val + "it begin. ", addEnd), randomTime())
};
const addContinue = (val: string, callback: Function) => {
    setTimeout(() => callback(val + "it continue. ", console.log), randomTime());
}
const addEnd = (val: string, callback: Function) => {
    setTimeout(() => callback(val + "it end. "), randomTime());
}
start("", addContinue);
Console verdict: at least it works

1. Pyramid of Doom

2. Spaghetti code

welcome to callback hell

How to escape callback hell ?

Let's try to re-discover Promises (~500l)

const double = (x: number) => x * 2;
const inc = (x: number) => x + 1;
console.log(inc(double(double(1)))); // not flat

step 1: learn clean chaining

const applyTo = (val: any, f: UnaryFn) => f(val);
const pipe = (...fns: UnaryFn[]) => (val: any) => fns.reduce(applyTo, val);
pipe(double, double, double, console.log)(1); // more flat than earth
1.Compose approach
2. Pipe approach

the art of flatten

const compose = (f: UnaryFn, g: UnaryFn) => (a: any) => f(g(a));
const quadruple = compose(double, double);
console.log(quadruple(1));// a little flatter

const composeN = (...fns: UnaryFn[]) => (val: any) => fns
    .reverse()
    .reduce((acc: any, cur: UnaryFn) => cur(acc), val);
const quadrupleThenIncThenLog = composeN(console.log, inc, double, double);
quadrupleThenIncThenLog(1); // very flat

Naive Sync Promise

export type UnaryFn = (a: any) => any;

const applyTo = (val: any, f: UnaryFn) => f(val);
const pipe = (...fns: UnaryFn[]) => (val: any) => fns.reduce(applyTo, val);

class NaiveSyncPromise {
  private _callbacks: UnaryFn[] = [];
  constructor(firstValue: any) {
    setTimeout(() => pipe(...this._callbacks)(firstValue));
  }
  then(callback: UnaryFn) {
    this._callbacks.push(callback);
    return this;
  }
}

const naiveSyncPromise = new NaiveSyncPromise("");
naiveSyncPromise
  .then((value) => value + "it begin. ")
  .then((value) => value + "it continue. ")
  .then((value) => value + "it end.")
  .then(console.log);

A wrapper to compose

Proposed Sync Promise

type NextPromise = (val: any) => SyncPromise;

class SyncPromise {
  constructor(public value: any) {}
  then(next: NextPromise) {
    setTimeout(() => {
      this.value = next(this.value).value;
    });
    return this;
  }
  map(f: UnaryFn) {
    return this.then((value) => new SyncPromise(f(value)));
  }
}

const syncPromise = new SyncPromise("");
syncPromise
  .then((value) => new SyncPromise(value + "it begin. "))
  .then((value) => new SyncPromise(value + "it continue. "))
  .map((value) => value + "it end.")
  .then((value) => { // tap ?
    console.log(value);
    return new SyncPromise(value);
  });

A wrapper to chain... and free monadic stuff !

Back to Async

Naive AsyncPromise 1/2

type NextPromise = (val: any) => NaiveAsyncPromise;

class NaiveAsyncPromise {
  private _callbacks: NextPromise[] = [];

  constructor(private handle: Handler) {
    setTimeout(() => {
      if (this._callbacks.length) {
        this._callbacks.reduce((acc: NaiveAsyncPromise, cur: NextPromise) => {
          const nextPromise = new NaiveAsyncPromise(acc.resolve(cur));
          nextPromise.handle(identity);
          return nextPromise;
        }, this);
      }
    });
  }

  private resolve(next: NextPromise) {
    return (resolve: any) =>
      this.handle(
        (result: any) => next(result).handle((x: any) => resolve(x)),
      );
  }

  then(next: NextPromise) {
    this._callbacks.push(next);
    return this;
  }
}

the convoluted implementation

Please, Kill me

Naive Async Promise 2/2

const naiveAsyncPromise = new NaiveAsyncPromise((resolve) => resolve(""));
naiveAsyncPromise
  .then((val: any) =>
    new NaiveAsyncPromise(
      (resolve) => setTimeout(() => resolve(val + "it begin. "), randomTime()),
    )
  )
  .then((val: any) =>
    new NaiveAsyncPromise(
      (resolve) =>
        setTimeout(() => resolve(val + "it continue. "), randomTime()),
    )
  )
  .then((val: any) =>
    new NaiveAsyncPromise(
      (resolve) => setTimeout(() => resolve(val + "it end. "), randomTime()),
    )
  )
  .then((val: any) =>
    new NaiveAsyncPromise(
      (_) => setTimeout(() => console.log(val), randomTime()),
    )
  );

the crazy chain

Bring back the Fantasy Monad

a design pattern to enlighten chaining

Fantasy Promise

type NextPromise = (val: any) => FantasyPromise;

class FantasyPromise {
  constructor(public handle: Handler) {}
  then(next: NextPromise) {
    return new FantasyPromise(
      (resolve: Function) => this.handle((a: any) => next(a).handle(resolve)),
    );
  }
  resolve() {
    return this.handle(identity);
  }
}

So fanciful...

const fantasyPromise = new FantasyPromise((resolve: any) => resolve(""));
fantasyPromise
  .then((s) =>
    new FantasyPromise(
      (resolve: any) =>
        setTimeout(() => resolve(s + "it begin. "), randomTime()),
    )
  )
  .then((s) =>
    new FantasyPromise(
      (resolve: any) =>
        setTimeout(() => resolve(s + "it continue. "), randomTime()),
    )
  )
  .then((s) =>
    new FantasyPromise(
      (resolve: any) => setTimeout(() => resolve(s + "it end. "), randomTime()),
    )
  )
  .then((s) => new FantasyPromise((_: any) => console.log(s)))
  .resolve();

If it's a Monad,

...And this is almost nothing

type NextPromise = (val: any) => MonadicPromise;

class MonadicPromise {
  constructor(public fork: Handler) {}
  static of(x: any) {
    return new MonadicPromise((resolve) => resolve(x));
  }
  chain(next: NextPromise) {
    return new MonadicPromise(
      (resolve: Function) =>
        this.fork(
          (a: Handler) => next(a).fork(resolve),
        ),
    );
  }
  map(f: Function) {
    return new MonadicPromise((resolve) =>
      this.fork((a: Handler) => resolve(f(a)))
    );
  }
  extract() {
    return this.fork(identity);
  }
}
const nextStr = (s: string) =>
  (prev: string) =>
    new MonadicPromise(
      (resolve: any) =>
        setTimeout(
          () => resolve(prev + s),
          randomTime(),
        ),
    );

const monadicPromise = MonadicPromise.of("");
monadicPromise
  .chain(nextStr("it begin. "))
  .chain(nextStr("it continue. "))
  .chain(nextStr("it end."))
  .map(console.log)
  .extract();

you can...

Monads implies...

after, all, alt, and, ap, attempt-p, attempt, bichain, bimap, both, cache, chain-rej, chain, coalesce, done, encase-p, encase, extract-left, extract-right, fork-catch, fork, future, go, hook, lastly, map-rej, map, node, pap, par, parallel, promise, race, reject-after, seq, swap, value...

Text

Call me Maybe

class Maybe {
  static of(x: any) {
    return new Maybe(x);
  }
  constructor(private _value: any) {}
  isNothing() {
    return (this._value === null);
  }
  map(f: Function) {
    return this.isNothing() ? 
    Maybe.of(null) : 
    new Maybe(f(this._value));
  }
  extract() {
    return this._value;
  }
  chain(f: (a: any) => Maybe) {
    return this.map(f).extract();
  }
  orElse(value: any) {
    if (this.isNothing()) {
      return Maybe.of(value);
    }
    return this;
  }
}
const document = {
  getElementById: (s: string) => s === "always" 
    ? s 
    : null,
  createElement: (s: string) => s,
  body: {
    appendChild: (s: string) => s,
  },
};

Maybe
  .of(document.getElementById("never"))
  .orElse(document.createElement("div"))
  .chain((el: any) => {
    const name = document.body.appendChild(el);
    return new Maybe(name);
  })
  .map(console.log);

even OO people likes monads

What about the rest of the three ?

so much stuff...

The end of the story ?

Can you tell me

  • Fantasy-land people have lost the Promise fight
  • Domenic Denicola never achieved Promise cancellation
  • Fantasy-land now exists:
    • ramda, folktale, most.js, daggy, sanctury, fluture...
  • Fantasy-land concepts continue to enlight FP in JS:
    • Observable, rxjs
    • Promise still expand but at what cost ?
    • new bridges to Haskell, Elm, Clojure, Reason...
    • design patterns for craftmanship
  • Fantasy-land concepts are in fact Maths/Logic concepts
    • they can't die
    • they guarantee quality and correctness of programs, a very trendy subject
  • Monads still a way to achieve what we want to avoid: side effect

bibliography

  • https://brianmckenna.org/blog/category_theory_promisesaplus
  • https://samsaccone.com/posts/history-of-promises.html
  • https://staltz.com/promises-are-not-neutral-enough.html
  • https://hgiasac.github.io/posts/2017-20-12-Are-Promises-Monads-Functor-or-Applicative.html
  • https://blog.jcoglan.com/2011/03/05/translation-from-haskell-to-javascript-of-selected-portions-of-the-best-introduction-to-monads-ive-ever-read/
  • https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/

Welcome to Fantasy Land

By Loïc TRUCHOT

Welcome to Fantasy Land

  • 544