Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

JSでHaskellのMaybeモナドを再現してみた

Haskell的な書き心地を再現しようと試みてみました。結果、かなりいい感じに仕上がりました。

// Haskellでは
// Just 3 >>= return . (+ 3);

// JSだと
Maybe.Just(3) .bind (compose(return_, v => v+3) ); // 似てる!!

Maybeモナド

結構えらい実装になりました。

class Maybe {
    constructor(type, value) {
        this.type = type;
        this.value = value;
    }
    bind(f) {
        if (this.type == 'Nothing')
            return Maybe.Nothing();
        return f.bind(this)(this.value);
    }
    static Just(v) {
        return new Maybe('Just', v);
    }
    static Nothing(f) {
        return new Maybe('Nothing', '');
    }
}

ここまでは良いのですが、以下はかなりテクニカルな実装になりました。。

function liftM(f) {
    // liftM f m1 = do x1 <- m1; return $ f x1
    return this.bind(x1 => this.return(f(x1)));
}
function ap(m) {
    // ap f m1 m2 = do x1 <- m1; x2 <- m2; return $ f x1 x2
    return this.bind(x1 => m.bind(x2 => this.return(x1(x2))));
}
Maybe.prototype.return = Maybe.Just;

// instance Applicative Maybe where
//     pure  = return
//     (<*>) = ap
Maybe.prototype.pure = Maybe.prototype.return;
Maybe.prototype['<*>'] = ap;

// instance Functor Maybe where
//     fmap = liftM
Maybe.prototype.fmap = liftM;

まず、return関数をMaybe.prototype.returnとして組み込みました。returnをstatic関数でなくメソッドにしたのは、利用する関数側からコンストラクタの特定をするのが面倒だからです。たとえばreturnがstaticなら、ap関数のなかでthisのコンストラクタを特定して、それのreturnを呼び出す流れになります。それよりも、thisに生えているreturnをそのまま呼び出すほうが楽ですよね。

次に、apやreturn_を関数として定義しています。これらはメソッドとして利用する想定なので(m.fmap(f)のように使いたい)、アロー関数で定義するとthisを参照できなくなります。そのため、functionキーワードを用いて定義しました。久しぶりにfunctionと書いた気がします。

Maybeモナドを使ってみる

使い心地はかなりHaskellに近くなっています。

function return_(...args) {
    return this.return(...args);
}

function compose(...fs) {
    return function (v) {
        for (const f of fs.reverse()) {
            v = f.bind(this)(v);
        }
        return v;
    };
}

// Just 3 `fmap` (*3)
Maybe.Just(3) .fmap (v => v*3);

// Just (*3) <*> Just 3
Maybe.Just((v) => (v*3)) ['<*>'] (Maybe.Just(3));

// Just 3 `bind` return . (+ 3)
Maybe.Just(3) .bind (compose(return_, (v) => v*3));

結構似ていますよね!!!我ながら頑張ったと思います。

まあでも、JSがデフォルトで部分適用できなかったり、演算子を関数として使えなかったり、言語仕様の時点でかなり再現が難しいので、こんなことはしなくて良いと思いました。