駆け出しエンジニアはPMFの夢を見るか?

駆け出しエンジニアの記録

『関数型プログラミングの基礎』を読んだ

関数型プログラミングの歴史から考え方を説明し、JavaScriptでサンプルコードを動かして関数型プログラミングとは何かを解説した本。

www.ric.co.jp

関数型言語で「モナド」というワードだけ知っていたが、具体的にどういうものか理解しておらず、雰囲気だけでも理解しておきたいなと思って読んだ。この本と併せて、別のWebの記事を読んでモナドの雰囲気は理解できたつもりなので、自分の理解をまとめておく。

モナドはコンテキストと呼ばれる文脈情報を持ちながら、値を取り扱うための抽象的な方法である。本書に登場するMaybeモナドは「値が存在するかどうか」という文脈情報を持ちながら、値を取り扱う際に必要な情報を提供し、処理の正確性や安全性を担保する役割を果たす。

// NOTE: モナドの定義に沿うならflatMap関数も定義する必要がある。
// NOTE: 型の定義の参考: https://zenn.dev/sterashima78/books/9dd0db90a6e532
type Just<T> = { type: "just"; value: T; };

type Nothing = { type: "nothing" };

type Maybe<T> = Just<T> | Nothing;

type Of = <T>(value: T) => Just<T>;

type OfNullable = <T>(value: T | undefined | null) => Maybe<T>;

const of: Of = (value) => ({ type: "just", value });
const ofNullable: OfNullable = (value) => value === undefined || value === null ? ({ type: "nothing"}) : ({ type: "just", value });

// 例外を使わないで除算ができない場合をMaybeで表現できる
const divide = (x: number, y: number): Maybe<number> => {
  if (y === 0) {
    return ofNullable(null) as Maybe<number>;
  } else {
    return of(x / y);
  }
};

MyabeモナドやListモナドやIOモナドなどがあり、モナドはそれらの具体例の総称である。総称であるということは、共通点があるということである。その共通点は、モナドインスタンスを生成するunit関数(of関数)と処理を合成するためのflatMap関数の2つを備えていることである。そして、unit関数flatMap関数モナド則を満たしている必要がある。モナド則とは、①右単位原則と②左単位原則と③結合則の3つである。

モナド高階関数を使って実装することができ、HaskellだけではなくScalaF#などでも使うことができる。仕事でよく使う言語であるJavaOptionalがあり、これもモナドの影響を受けている(of関数flatMap関数がある)。Optionalを使えば、例外(= 参照透過性を破壊するもの)を回避して処理を書くことができる。JavaOptionalがあることは知っていたが、関数型プログラミングにおけるモナドの概念に影響を受けて設計された機能の一つであることは知らなかった。。

public class Example {
    // NOTE: Optionalを使うことでmain関数から例外(副作用)を除いている
    // 標準出力に出力を行っているので参照透過性のある関数ではない
    public static void main(String[] args) {
        Optional<String> content = readFile("example.txt");
        if (content.isPresent()) {
            System.out.println(content.get());
        } else {
            System.out.println("File not found or is empty.");
        }
    }

    public static Optional<String> readFile(String fileName) {
        try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append(System.lineSeparator());
            }
            return Optional.of(sb.toString());
        } catch (IOException e) {
            return Optional.empty();
        }
    }
}

数学的背景や実装方法までは理解できていないが、これまで自分がやってきたことや知識がモナドと紐づいて、高尚で触れ難い概念から身近な概念に変わった。

(余談)関数型言語の勉強をしてモナドのところに入ると、IOモナドの話が出てくる。IOモナドを使うと副作用をなくすことができるといった内容が書いてあり、サンプルコードを見ると入力や出力を行っていて、「やっぱり副作用あるじゃん!?」となり挫折していた。この記事を読んで、観測している立場によって副作用がある/ないが変わることが理解できたので、備忘録として書いておく(この記事を読むとHaskellは評価器には副作用はないと書いてあるが、他の言語だと評価器で副作用があるということなのか?という疑問は残った。今回はモナドの雰囲気を理解することが目的なので、別の機会に調べたり勉強したりしたいと思う。)。

この立場の説明では、Haskell には副作用はない。なぜなら、Haskell が作るのは命令書のみで、それが実行されるのは Haskell の外での話だからだ。 たとえば、getChar :: IO Char は、実行されるごとに(同じこともあるが)別の文字を返す。それでも Haskell には副作用はない。Haskell が作り出すのは、「標準入力から文字を読み込め」という命令書だけであって、実行はしないからだ。

kazu-yamamoto.hatenablog.jp

(2023/05/04 追記) ありがたいことに前職の後輩が読んでコメントをくれたので、追記する。 最近、TypeScriptでResult型の提案の記事が出ていて、どうしてその発想に至ったのか気になるようになった。 ぱっと調べてみると、Result型はRustの型としてあり、Rust型はHaskell関数型言語のMaybeモナドなどに影響を受けたらしい。ということで、「関数型プログラミングの基礎」を読むことにした。

Javaだとthrows句があり、呼び出し側が例外を処理することを強制させられるので、呼び出し側が例外の処理を書き忘れることを防げる。が、TypeScriptではthrows句がなく、呼び出し側が例外を処理することを忘れる可能性がある。ではどうするかとなった時に、Result型に値をラップしてちゃんと例外の処理を忘れないようにしようというアプローチが出てきたのかなと思った(呼び出される関数の返り値の型をユニオン型にして値と例外を繋げて行く方法もあるが、例外が多い時に呼び出し側が書かなければならないコード数が増えそうな気がする)。また、Result型を使った書き方をすれば、より関数型プログラミングパラダイムに近い書き方ができるので、広く指示を得ているのではないかと思った。