ひびのログ

日々ではないけどログを出力していくブログ

わかモナ ~わかる!数式が出てこないモナドへの入り口~

プログラミングをしている画面です。

数年前から関数型プログラミングが流行り始めて、やってみよう! という人もいらっしゃるかと思います。なにを隠そう、私がその口です。

そして Lisp や Haskell なんかに手を出した時、「モナド」という言葉が出てくるかもしれません。

知らないことは調べ尽くすのがプログラマの性。

しかし! モナドの意味を調べてみると、「計算を表現する構造」だったり「函手(かんしゅ)」だったり「ポケモン」だったりと、なんだかよくわからないことばかり書いてあるかと思います。

そこで、オブジェクト指向から関数型に入門し、モナドがなんとなーくわかった気になれるような説明をしたいと思います!

注意事項

自分の理解や説明の関係上、モナドを知っている人から見ると「間違ってる!」と思われるかもしれません(モナド則とか)。 全く知らない人が理解する取っ掛かりとして書きましたので、ある程度は見逃していただけるとうれしいです。 ただ、思いっきり間違っている場合は指摘していただけると嬉しいです。

なので、この内容を読んだだけで完全にわかった気にならないでください。 あくまで理解のとっかかりです。 間違っていても責任は取りません。

例は基本的に JavaScript で記述しています。

前提:モナドは理解できなくてもいい

理解していなくても、いつの間にか使っているので。 実際に使わなきゃいけない言語って少ないですし。必須なのはHaskelとかですかね? 書いたことないですけど。

関数型プログラミングを簡単に説明

そもそも関数とは

関数型プログラミングの本を見ると、大体見開き1ページ目に f(x) = x + aとか書いてありますけど、わからなくていいです。 私たちはプログラマであって、数学者じゃないんです。

ひとまず、プログラミングの文脈における「関数」という理解でいいです。 引数をとったりとらなかったりして、値を返したり返さなかったりするアレです。

ただし、オブジェクト指向ではないので、オブジェクトとかクラスとかはありません。 そのため、「メソッド」は存在しません。

関数型言語の重要な性質(前提知識)

  • First class function(第一級関数・高階関数)
  • 参照透過性
  • 副作用がない

難しい単語が出てきましたが、説明するのでチャンネルはそのままで!

First class function(第一級関数・高階関数)

今回の主旨とは関係ありませーん。

オブジェクト指向ではオブジェクトが変数に代入できるように、 関数型言語では関数が変数に代入できます。

// 変数に関数を代入する例
const func = () => console.log("呼ばれたよ");
func() // 出力:呼ばれたよ

参照透過性

関数を同じ引数で呼び出したら必ず同じ値が返ってくることです。 例えば、関数の中で引数同士の足し算をするだけなら参照透過ですが、 受け取った値にランダムな数を足すのは参照透過ではありません。

// 参照透過な関数
const func = (a, b) => a + b;
// 参照透過ではない関数
// 呼び出しごとにランダムな値が使われるため、結果が毎回変わる
const func = (a) => a + Math.random();

// 変数bの値が変わると、func2の結果も変わる
const b = 1;
const func2 = (a) => a + b;

副作用

大雑把に言えば、関数の中で戻り値以外に影響をあたえることです。 例えば、画面出力をしたり、関数の外にある変数を書き換えたりです。

// 副作用がない関数
const func = (a) => a + 1;

const func2 = (num) => num + 1;
const num1 = 1;
const num2 = func2(num1);
// 副作用がある関数
// 副作用:コンソールに文字を出力している
const func = (a) => {
    console.log("呼ばれたよ");
    return a + 1;
}

// 副作用:関数の外で定義されている変数を変更している
let num = 1;
const func2 = () => num++;

関数型言語は、手続き型言語ではない

見た感じだと手続き型と変わらないですが、実はまったく違うものです。 関数型の世界は、参照透過であり副作用がないという、手続き型とは違う世界なのです。

手続き型の常識は、PCと一緒に窓から投げ捨ててください。その後、PCを回収してきてください。


モナドとは?

いよいよモナドの説明です。

ここでいう「モナド」は、「とある関数」と読み替えて構いません。 あとで詳しく触れます。

関数型の世界は、「参照透過」であり「副作用がない」と説明しました。 そして、副作用の代表的な例に、「画面出力」が挙げられます。 画面出力は関数の中で戻り値以外に影響をあたえることなので副作用です。

では、関数型言語では画面に文字を出力できないのか?


答えはNoです。 なぜなら、モナドがあるからです。

モナドは、副作用を起こすときに使用します。


逆に言うと、モナドがあると、関数型の世界で副作用が起こせます。

ついでに、モナドは参照透過でなくてもかまわないです。 参照透過で副作用がない世界の中で、唯一参照透過でなくても、副作用があってもいい、それがモナドです。

ちなみに手続き型(オブジェクト指向)では副作用を起こせるので、モナドは必要ありません。

モナドの種類

上述の通り、モナドは参照透過ではなかったり副作用を起こしたりする際に使用します。

ですので、副作用の種類などによって、モナド自体の種類も変わります。

Maybe モナド

例外処理です。

例外というのは、引数でも戻り値でもありません。 ですから、関数型言語では扱えません。

そこで、「処理に失敗するかもしれない処理を行う」ために、Maybe モナドを使用します。

const maybeMonad = () => {
    try {
        const ret1 = mayFailToMaybe1(); // 失敗するかもしれない処理
        mayFailToMaybe2(ret1); // 失敗するかもしれない処理
        return "success";
    } catch (e) {
        return "error";
    }
};

処理に成功したら"success"が、処理に失敗したら"error"がそれぞれ返ってきます。 この関数は、引数が同じなのに戻り値が違うため、参照透過ではない関数です。

ですが、モナドという大義名分で、関数型の世界でも使用できます。

Java とか Scala とか Kotlin とかでは、OptionalとかOptionクラスとしておなじみです。あれです。

IO モナド

「画面に文字を出力する」という副作用を起こすためのモナド(関数)です。

const outputMonad = (str) => console.log(str) || str;

まとめ

  • 関数型言語は、参照透過で、副作用がない
  • そのため、コンソールに文字を出力したりできない
  • モナドを使用すると副作用が起こせるので、コンソールに文字を出力したりできる
  • モナドは、関数型言語において副作用を起こすために使う
  • 同様に、参照透過でない関数がモナドとして利用できる

一言で

参照透過で副作用がない関数型という世界の中で、参照透過でない・副作用がある処理を記述するときに使う(大事なことなので)。

おまけ

関数型の世界は、「参照透過」であり「副作用がない」ので、 コンパイル時にすべての値が計算できます。 「定数畳み込み」という最適化技法です。

もちろん手続き型のコンパイラでも行なっているとは思いますが、 関数型では前述した理由により定数とみなせる場合が多いので、 最適化が高いレベルで適用できます。

おまけ2

個人の感想ですが、最近流行りの Flux は、 「状態を保持する」という副作用をまとめた、状態モナドの一種とも考えられると思います。

Redux で言うと、returndispatch>>=は Reducer(state から値を取ってきて、新しい state を返す)

ついでに、Middleware というモナドを挟むことで副作用も実現しています。 (個人的には ActionCreator でやりたいところではありますが)