LoginSignup
27
18

More than 5 years have passed since last update.

JavaScript + generator で Maybe、 Either、 Promise、 継続モナドと do 構文を実装し async-await と比べてみる

Last updated at Posted at 2018-02-02

モナドを定義する

モナドという性質をもつ型は

  • 二項演算 >>= (bind)
  • 一項演算 pure

を持ちます。

class Monad{
  // pure :: a -> m a
  static pure(a){ throw new Error("abstruct"); }
  // (>>=) :: m a -> (a -> m b) -> m b
  bind(f){ throw new Error("abstruct"); }
  // bind(f){ return this.constructor.join(this.constructor.map(f(this))); }

  // join :: m m a -> m a
  //static join(mma){ return mma.bind((ma)=> ma); }
  // (<$>) ::  m a -> (a -> b) -> m b
  //map(f){ return this.bind((a)=> this.constructor.pure(f(a))); }
}

do 構文を定義する

function _do(M, genfn){
  const gen = genfn();
  return recur(gen, null);
  function recur(gen, prev){
    const {value, done} = gen.next(prev);
    const ma = value instanceof M ? value : M.pure(value);
    return ma.bind((a)=> !done ? recur(gen, a) : M.pure(a) );
  }
}
  • JS の do は予約語なので _do にしてます
  • do 構文は、モナディックな型の値 m1, m2, m3 を逐次返す generator を引数にとる
  • そして ma.bind((a)=> mb.bind((b)=> mc.bind((c)=> Monad.pure(c) ))) のように計算をつなげる
  • _do の第一引数にモナド型のコンストラクタを渡しているのは
    • const ma = value instanceof M ? value : M.pure(value); のために使っている
    • returnyield の違いを吸収するために必要
    • 本当は return pure(a)yield pure(a) が正しい
    • haskell の return にシャレて return a と書けるようにしている
    • co や async-await は awaitreturnPromise に強制的に包む仕様なので(非同期実行のため)それにあわせた
    • while ではなく recur してるのも非同期実行されることを意識しているから

Maybe モナドを定義する

Maybe の >>= (bind) の性質は、計算結果が nothing だと計算を打ち切って nothing を返します。

class Maybe extends Monad {
  constructor(a){ super(); this.a = a; }
  isJust(){ return this.a != null; }
  isNothing(){ return this.a == null; }
  bind(cont){ return this.isJust() ? cont(this.a) : Maybe.nothing(); }
  static pure(a){ return Maybe.just(a); }
  static just(a){ return new Maybe(a); }
  static nothing(){ return new Maybe(null); }
  toString(){ return this.isJust() ? `Just ${this.a}` : `Nothing`; }
}

Maybe モナドを使ってみる

すべて just なら成功。

function main(){
  const opt = _do(Maybe, function* calc(){
    const a = yield Maybe.just(1);
    console.assert(a === 1);
    const b = yield Maybe.just(1);
    console.assert(b === 1);
    const c = yield Maybe.just(a + b);
    console.assert(c ===  1 + 1);
    return c;
  });
  console.assert(opt.toString() === "Just 2");
}
main();

ひとつでも nothing があれば nothing。

function main(){
  const opt = _do(Maybe, function* calc(){
    const a = yield Maybe.just(1);
    console.assert(a === 1);
    const b = yield Maybe.nothing(); // なんか処理がうまくいかなかった
    console.assert(b === 1);
    const c = yield Maybe.just(a + b);
    console.assert(c ===  1 + 1);
    return c;
  });
  console.assert(opt2.toString() === "Nothing");
}
main();

null 安全なコードが書けた。

Either モナドを定義する

Either モナドの >>= (bind) の性質は、計算結果が left のときは計算を打ち切って left 値を返します。

class Either extends Monad {
  constructor(l, r){ super(); this.l = l; this.r = r;  }
  isLeft(){ return this.l != null; }
  isRight(){ return this.r != null; }
  bind(cont){ return this.isRight() ? cont(this.r) : Either.left(this.l, null); }
  static pure(a){ return Either.right(a); }
  static right(a){ return new Either(null, a); }
  static left(a){ return new Either(a, null); }
  toString(){ return this.isLeft() ? `Left ${this.l}` : `Right ${this.r}`; }
}

Either モナドを使ってみる

すべて right なら計算が最後まで続く

function main(){
  const opt = _do(Either, function*(){
    const a = yield Either.right(1);
    console.assert(a === 1);
    const b = yield Either.right(1);
    console.assert(b === 1);
    const c = yield Either.right(a + b);
    console.assert(c ===  1 + 1);
    return c;
  });
  console.assert(opt.toString() === "Right 2");
}
main();

途中で処理が失敗(=left)したらそこで計算を打ち切って left を返す

function main(){
  const opt = _do(Either, function*(){
    const a = yield Either.right(1);
    console.assert(a === 1);
    const b = yield Either.left("fail"); // なんか処理が失敗した
    console.assert(b === 1);
    const c = yield Either.right(a + b);
    console.assert(c ===  1 + 1);
    return c;
  });
  console.assert(opt.toString() === "Left fail");
}
main();

失敗つき計算を表現できた。うれしい!

Promise モナド

Promise はJSの実装そのまま使ってみます。

Promise.prototype.bind = Promise.prototype.then;
Promise.pure = Promise.resolve;
Promise.prototype.map = Monad.prototype.map;
Promise.join = Monad.join;
  • JS の Promiseはネストできないので map を定義しても then と同じ
  • JS の Promiseはネストできないので join を定義するのはあまり意味がない
    • Promise.resolve(Promise.resolve(1))Promise.resolve(1) として扱われる

Promise モナドを使ってみる

function main(){
  const prm = _do(Promise, function*(){
    const a = yield Promise.resolve(1);
    console.assert(a === 1);
    const b = yield Promise.resolve(1);
    console.assert(b === 1);
    const c = yield Promise.resolve(a + b);
    console.assert(c ===  1 + 1);
    return c;
  });
  // JS の Promise はそのイベントループの中作られた Promise 値は次のイベントループまで pending になるため中を見るには then する必要ある
  prm
    .then((c)=> console.assert(c === 2))
    .catch((c)=> console.assert(false));
}
main();

これが、 async-await の実装の基礎です (実際は try-catch も使えるようになっているので少し違う)

try-catch できる async-await の実装

function async(genfn) {
  return (...args)=>{
    const gen = genfn.apply(this, args);
    return recur(gen, null, null);
    function recur(gen, prev, preverr){
      try{
        const {value, done} = preverr == null ? gen.next(prev) : gen.throw(preverr);
        const ma = value instanceof Promise ? value : Promise.resolve(value);
        return ma
          .then((a)=> !done ? recur(gen, a, null) : Promise.resolve(a) )
          .catch((err)=> !done ? recur(gen, null, err) : Promise.reject(err) );
      }catch(err){
        recur(gen, null, err);
      }
    }
  };
}

do との違いとして

  • generator 関数を promise 値関数に変換するために return (...args)=>{ している
  • generator の実行を try-catch で囲んでいる
  • err が扱えるように recur が 3 引数になった
  • generatir.throw で generator 関数へ throw している

などがあります。

async-await を使う

const main = async(function*(init){
  const a = yield Promise.resolve(init);
  console.assert(a === init);
  const b = yield Promise.resolve(1);
  console.assert(b === 1);
  const c = yield a + b;
  console.assert(c ===  1 + init);
  return c;
});

main(1)
  .then((c)=> console.assert(2 === c))
  .catch((c)=> console.assert(false))

main に引数が付きました。

もちろんエラー対策も万全なので

const main = async(function*(init){
  let a;
  try{
    throw new Error("some error"); 
    a = yield Promise.resolve(init);
  }catch(err){
    // catch できる
    a = yield init; // resolve なくてもいける
  }
  console.assert(a === init);
  let b;
  try{
    b = yield Promise.reject(new Error("some AIO erorr"));
  }catch(err){
    // reject も catch できる
    b = yield Promise.resolve(1);
  }
  console.assert(b === 1);
  const c = yield a + b;
  console.assert(c ===  1 + init);
  return c;
});

main(1)
  .then((c)=> console.assert(2 === c))
  .catch((c)=> console.assert(false))

のような処理も実行できます

do と async の diff

継続モナドを実装する

こうなってくるとわけがわからない

class Cont extends Monad {
  // runCont :: ((a -> r) -> r)
  constructor(runCont){ super(); this.runCont = runCont; }
  //  (Cont c) >>= f = Cont $ \k -> c (\a -> runCont (f a) k)
  bind(f){ return new Cont((k)=> this.runCont((a)=> f(a).runCont(k) ) ); }
  // return a       = Cont $ \k -> k a
  static pure(a){ return new Cont((k)=> k(a)); }
  // callCC :: ((a -> m b) -> m a) -> m a 
  static callCC(f){ return new Cont((k)=> (f((a)=> new Cont((_)=> k(a)) )).runCont(k) ); }
}

継続モナドを使ってみる

緊急脱出ができる。

function main(){
  return ((c)=> c.runCont((a)=>a))( _do(Cont, function*(){
    const ret = yield Cont.callCC((exit)=> _do(Cont, function*(){
      const a = yield Cont.pure(1);
      const b = yield Cont.pure(1 + a);
      console.assert(b === 2);
      if(b === 2){
        yield exit(100); // escape!
      }
      return Cont.pure(-100); // never run
    }));
    console.assert(ret === 100);
    return Cont.pure(ret);
  }));
}
console.assert(main() === 100);

exit で抜けた場合と return pure で抜けた場合で型が同じなので例外というよりは goto のような使い方になりそう。

後記

  • xstreamモナドとかjQueryモナドとかリストモナドも書きたかった。あとで書くかも。
  • 複雑になると haskell より醜い

元ネタ https://qiita.com/DUxCA/items/77a36b7d2b75d8278f9d

27
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
18