⛓️

Rustのメソッドチェーンを使った関数型プログラミング

2025/03/04に公開

他のプログラミング言語を使っている人がRustを始めると、いくつか発想の転換が必要になります。

そのうちの一つ、Rustのメソッドチェーンについて私が気づいたことを記事にします。

Rustはメソッドチェーンが重要な要素

Rustはメソッドチェーンが頻繁に利用される言語です。むしろメソッドチェーンを基準として言語設計されているように見えます。

例えばイテレータを使うときはこんな感じになります。

exes.into_iter()
    .filter_map((|x| ...)
    .collect()

他にもawaitは後置記法になっており、メソッドを繋げやすくなっています。

let data = x.find(...).await?.ok_or_else(|| ...)?;

他のプログラミング言語でもメソッドチェーンは存在していますが、特定のライブラリに依存することがほとんどです。そして多くの場合はメソッドが自分自身を返すことでメソッドチェーンを実現します。
RustでもBuilderパターンがそれにあたり、多くのライブラリが採用しています。

let client = reqwest::Client::builder()
    .default_headers(headers)
    .https_only(true)
    .build()?;

この自分自身を返すだけでなく、Rustは自分自身を返さない場合でもメソッドチェーンが積極的に利用されるのです。

関数型プログラミングのパイプライン

さて、話はRustではなく関数型プログラミング言語に移動します。関数型プログラミング言語にはパイプラインというものがあります。

関数型プログラミングではある1つの変数に対して関数を次々に適用させて新しい値を得ることがあります。それを式で書くとこのようになります。

let y = f3(f2(f1(x)));

括弧のネストが深いので視認性が悪いですね。そこで一部のプログラミング言語ではパイプライン演算子が採用されています。パイプライン演算子|>を使って同じ処理を書くとこうなります。

let y = x |> f1 |> f2 |> f3;

パイプライン演算子は大変便利なのですが、採用されているプログラミング言語は少ないです。理由は、複数の引数を受け取る関数に利用しずらいからです。
1つの引数を受け取る場合にはとても分かりやすいのですが、複数の引数を受け取る場合は途端に分かりずらくなります。
いくつかのテンプレートエンジンではパイプライン演算子を採用しつつ、複数の引数を受け取ることも可能にしていますが、構文は分かりやすいとは言い難いです。例えばLiquidはこんな感じです。

{% assign kitchen_products = products | where: "type", "kitchen" %}

Rustのメソッドチェーンはパイプラインにもなる

パイプラインのこの形、メソッドチェーンに似てると思いませんか。

let y = x |> f1 |> f2 |> f3;

Rustはパイプライン演算子を使わずに、メソッドチェーンでパイプラインを実現しているのです。メソッドチェーンで書き直すとこうなります。

let y = x.f1().f2().f3();

これによって複数の引数を受け取っても、普通のメソッドと同じなので視認性が低下しません。

メソッドチェーンによる検索性の向上

メソッドチェーンでパイプラインを実現することで、複数引数への対応だけでなく、Rustはメソッドの検索性を向上させています。

変数をある型から別の型に変えたいときに、多くのプログラミング言語ではそれを行う関数を探してググったり、AIに訊いたり、過去に作ったファイルを見返したりする必要があります。
しかし、Rustではドット.を打つだけで、テキストエディタによって使えるメソッドが全て出てきます。その中から探すだけで良いので、今開いているファイルから離れる必要がありません。

型がネストしてもメソッドチェーンを使えば大丈夫

Rustには沢山の型があり、沢山のトレイトがあります。そしてそれらが何層にもネストします。例えば簡単なものでこんな感じです。

Future<Result<Option<String>, Error>>

最初はかなり面食らいますが、これはメソッドチェーンで外側から一つづつ剥がしていけば良いだけということが分かります。言い方を変えれば、メソッドチェーンで処理する余地をくれているのです。

Futureをawaitで、Resultをunwrap()?などで、OptionでNoneを許容しないならok_or_else()などで剥がせば値にたどり着きます。

let v = x.await.unwrap().ok_or_else(|| ...).unwrap();

トレイトでメソッドを増やせる

ある型から別の型に変換する関数を独自に作れるように、ある型から別の型に変換するメソッドを独自に作ることができます。それは、自分で作った型に限らず、誰かが作ったライブラリの型に対しても可能です。メソッドチェーンに好きなように介入することが可能なのです。

それにはトレイトを使います。

例えばanyhowのエラーに、HTTPのエラーコードを得るメソッドを追加したいときはこんなような感じで実装していきます。

trait HTTPErrorCode {
    fn get_code(&self) -> u16;
}

impl HTTPErrorCode for anyhow::Error {
    fn get_code(&self) -> u16 {
        ...
    }
}

// 使い方 -> result.unwrap_err().get_code()

関数に比べると、トレイトを作らないといけないので少し記述量が多くなりますがメソッドチェーンの恩恵を得られるようになります。

itertoolsはこの方法でイテレータを拡張した機能を提供していますね。

以上です。
他にもRustをやっていく中での気づきを記事にしていけたらと思います。

Discussion