無駄と文化

実用的ブログ

やってみよう!メモリリーク

メリークリスマス!
この記事は はてなエンジニア Advent Calendar 2025 の3日目の記事です。

大遅刻ですね。すごいことですよこれは。

 


さて、Rust ネタです。

ご存知の通り Rust には GC がありません。代わりに所有権システムがあり、所有権を持つ変数がスコープを抜けるときに値がドロップされ、メモリが解放されます。
とはいえ通常のドロップ処理を回避したり相互に参照する値を作って変数を無限に長生きさせると Rust でもメモリは漏れます。

今日はメモリリークする Rust コードを実際に書いて、書き味を確かめてみましょう。

 

Rc<T> と相互参照

素朴な参照カウンター式のガベージコレクターはどこからか値が参照されている限りメモリを解放しません。
そのため相互に参照する値を作ると参照カウンターが0にならず、回収されない値を作れます。

JavaScript 風の仮想言語で書くとこんな感じ。

class Node {
  next = null;
}

function main() {
  const a = new Node();
  const b = new Node();

  a.next = b;
  b.next = a;
}
// ↑↑ ここで main は a, b の参照を辞める
// しかし a は b を参照し続け、b は a を参照し続ける
// 結果として a, b の参照が残り続ける

main();

※ 実際の JavaScript ランタイムのガベージコレクターは賢いので、上記の実装でも適切にメモリを回収してくれます

 

Rust で上記のようなコードを書いてみましょう。
Rust には参照カウント方式のメモリ管理を提供する Rc<T> 型があります。ただし Rc<T> だけを素朴に使っても循環参照は作れません。

use std::rc::Rc;

struct Node {
    next: Option<Rc<Node>>,
}

fn main() {
    let a = Rc::new(Node { next: None });
    let b = Rc::new(Node { next: None });

    *a.next = Some(Rc::clone(&b));
//  ^^^^^^^ can't be dereferenced
//  error[E0614]: type `Option<Rc<Node>>` cannot be dereferenced
    *b.next = Some(Rc::clone(&a));
//  ^^^^^^^ can't be dereferenced
//  error[E0614]: type `Option<Rc<Node>>` cannot be dereferenced
}

Rc<T> は参照先を不変にするので上記のように可変参照を借用することはできません。

...ということが 書籍『プログラミング Rust』 に書いてあって。RefCell<T> による 内部可変 を使えばこの制限を回避できるとも書かれています。
やってみましょう。

use std::cell::RefCell;
use std::rc::Rc;

struct Node {
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    {
        let a = Rc::new(Node {
            next: RefCell::new(None),
        });
        let b = Rc::new(Node {
            next: RefCell::new(None),
        });

        *a.next.borrow_mut() = Some(Rc::clone(&b));
        *b.next.borrow_mut() = Some(Rc::clone(&a));
    }
    // ↑↑ ここで a, b のスコープ終わり "変数" が所有権を失う
    // しかし循環参照により Rc<Node> の "値" はドロップされずに残る
}

理屈ではこれでメモリが漏れるはず。とはいえ実際に目で見える形でないと面白くありませんね。
可視化してみましょう。

 

メモリリークを目で見る

やはりプログラムの実行中にメモリ消費が増えていく様を見るほうが実感が湧きますね。
もっと大きめのメモリをリークさせるプログラムを走らせつつ、ps u コマンドで RSS, VSZ あたりを観察してみましょう。

さきほどの Rust コードを改造します。

use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;

struct Node {
    #[allow(dead_code)]
    payload: Vec<u8>,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let payload_bytes: usize = 8 * 1024 * 1024; // 8MiB

    let mut i: u64 = 0;
    for _ in 0..100 {
        {
            let a = Rc::new(Node {
                payload: vec![0u8; payload_bytes],
                next: RefCell::new(None),
            });

            let b = Rc::new(Node {
                payload: vec![1u8; payload_bytes],
                next: RefCell::new(None),
            });

            // 循環参照を作る
            *a.next.borrow_mut() = Some(Rc::clone(&b));
            *b.next.borrow_mut() = Some(Rc::clone(&a));
        }
        // ↑↑ ここで a, b のスコープ終わり "変数" が所有権を失う
        // しかし循環参照により Rc<Node> の "値" はドロップされずに残る

        i += 1;

        if i % 10 == 0 {
            println!("iter={}", i);
        }

        // 観察しやすいように少し待つ
        std::thread::sleep(Duration::from_millis(100));
    }
}

構造体に8MiBずつの配列を持たせて循環参照させます。それを100回繰り返します。

ログを取るためのスクリプトも用意しましょう。

#!/usr/bin/env bash

# 使い方:
#   ./log_mem.sh <PID>
#
# 例:
#   ./leaky_program &
#   pid=$!
#   ./log_mem.sh "$pid"

set -eu

pid="${1:-}"

if [ -z "$pid" ]; then
  echo "Usage: $0 <PID>" >&2
  exit 1
fi

log_file="mem_${pid}.csv"
echo "timestamp,rss_kb,vsz_kb" > "$log_file"

while kill -0 "$pid" 2>/dev/null; do
  ts=$(date +"%Y-%m-%dT%H:%M:%S")
  read _ rss vsz <<<"$(ps -o pid= -o rss= -o vsz= -p "$pid")"
  echo "$ts,$rss,$vsz" >> "$log_file"
  sleep 1
done

echo "process $pid finished; log saved to $log_file"

これを log_mem.sh という名前で保存し、計測してみます。

$cargo run --release &              
pid=$!
./log_mem.sh "$pid"

[1] 13333
   Compiling leak-drill v0.1.0 (/Users/todays_mitsui/private/leak-drill)
    Finished `release` profile [optimized] target(s) in 0.50s
     Running `target/release/leak-drill`
iter=10
iter=20
iter=30
iter=40
iter=50
iter=60
iter=70
iter=80
iter=90
iter=100
[1]  + done       cargo run --release
process 13333 finished; log saved to mem_13333.csv

結果はこうなりました。

timestamp rss_kb vsz_kb
2025-12-24T21:54:10 16448 410363040
2025-12-24T21:54:11 34000 410332416
2025-12-24T21:54:12 173632 410870016
2025-12-24T21:54:13 329312 411001088
2025-12-24T21:54:14 468640 411133184
2025-12-24T21:54:15 624352 411395328
2025-12-24T21:54:16 771904 411658496
2025-12-24T21:54:17 894880 412051712
2025-12-24T21:54:18 1058720 412051712
2025-12-24T21:54:19 1222560 412051712
2025-12-24T21:54:20 1370080 412313856
2025-12-24T21:54:21 1517536 412313856

RSS が順調に増えて、どんどんメモリを食っているのが見えますね!

 

Vec<T>.leak()

相互参照以外の方法も試しましょう。

Rust の可変長配列 Vec<T>.leak() というメソッドを実装しています。 .leak() は配列が確保しているヒープ領域を意図的に解放せず、 &'static mut [T] の形でスライスを取り出すことを許します。
シングルクォートから始まる 'aライフタイム 指定子 で、その中でも 'static は特別な「プロセスの全期間」を表すライフタイムです。つまり .leak() によって返されたスライスはプロセス実行中に解放されないことが約束されます。

use std::hint::black_box;

fn vec_leak(payload_bytes: usize) -> &'static [u8] {
    let v = vec![0u8; payload_bytes];
    black_box(&v);
    v.leak()
}

この関数は指定された長さの配列を作り、 .leak() を呼んでスライスを返します。
関数の中で定義された変数 v は、普通なら関数スコープの最後で所有権を失うか関数呼び出し元に所有権を move させるのですが。 .leak() によって意図的に所有権を放棄しているため関数スコープを抜けてもメモリが解放されません。

先ほどと同じく、この関数を100回呼んで計測しましょう。

use std::hint::black_box;
use std::time::Duration;

fn vec_leak(payload_bytes: usize) -> &'static [u8] {
    let v = vec![0u8; payload_bytes];
    black_box(&v);
    v.leak()
}

fn main() {
    let payload_bytes: usize = 8 * 1024 * 1024; // 8MiB

    let mut i: u64 = 0;
    for _ in 0..100 {
        {
            let _slice = vec_leak(payload_bytes);
        }
        // ↑↑ ここで _slice のスコープが終わるが、_slice は借用なので所有権は失われない
        // もはやメモリは誰のものでもない

        i += 1;

        if i % 10 == 0 {
            println!("iter={}", i);
        }

        // 観察しやすいように少し待つ
        std::thread::sleep(Duration::from_millis(100));
    }
}

結果はこうなりました。

timestamp rss_kb vsz_kb
2025-12-24T22:20:06 16512 410494112
2025-12-24T22:20:07 50384 410462464
2025-12-24T22:20:08 107872 410855680
2025-12-24T22:20:10 165296 411117824
2025-12-24T22:20:11 239056 411248896
2025-12-24T22:20:12 312816 411379968
2025-12-24T22:20:13 394736 411379968
2025-12-24T22:20:14 476656 411379968
2025-12-24T22:20:15 542224 411511040
2025-12-24T22:20:16 607824 411773184
2025-12-24T22:20:17 689744 411773184

いいですね。

 

ManuallyDrop<T> とドロップ忘れ

std::mem::ManuallyDrop という構造体があります。変数の所有権が失われても自動でドロップ処理が呼ばれず、名前の通り手動でのドロップが必要になります。

let mut s = ManuallyDrop::new(String::new("Hoge"));

unsafe {
    ManuallyDrop::drop(&mut s);
}

忘れずに ManuallyDrop::drop() するのは実装者の責任です。これを忘れるとメモリは解放されずリークします。

手動ドロップをせずに100回呼んで観察してみましょう。

use std::hint::black_box;
use std::mem::ManuallyDrop;
use std::time::Duration;

fn main() {
    let payload_bytes: usize = 8 * 1024 * 1024; // 8MiB

    let mut i: u64 = 0;
    for _ in 0..100 {
        {
            let mut v = ManuallyDrop::new(vec![0u8; payload_bytes]);
            black_box(&mut v);

            // unsafe {
            //     ManuallyDrop::drop(&mut v);
            // }
        }
        // ↑↑ ここで v のスコープが終わる、v は ManuallyDrop なので所有権が失われても自動的にドロップされない
        // ManuallyDrop::drop() を呼び忘れているのでメモリは残り続ける

        i += 1;

        if i % 10 == 0 {
            println!("iter={}", i);
        }

        // 観察しやすいように少し待つ
        std::thread::sleep(Duration::from_millis(100));
    }
}

結果はこう。

timestamp rss_kb vsz_kb
2025-12-24T23:43:17 32 410060368
2025-12-24T23:43:18 1264 410593536
2025-12-24T23:43:19 66976 410864896
2025-12-24T23:43:20 140736 410995968
2025-12-24T23:43:21 222656 410995968
2025-12-24T23:43:22 288240 411127040
2025-12-24T23:43:23 362000 411258112
2025-12-24T23:43:24 435760 411389184
2025-12-24T23:43:25 517680 411389184
2025-12-24T23:43:26 591440 411520256
2025-12-24T23:43:27 629712 411651328

はい素晴らしい。

 

まとめ

今回はさまざまな方法でメモリリークするコードを動かして観察してみました。
Vec::leak(): &'static [T]ManuallyDrop<T> など、メモリを漏らすのにも型表現がついていてお行儀がいいですね。

 

 

私からは以上です。

 

参考

Special Thanks.

Grass言語でHello,world!に挑戦

Grass 言語というプログラミング言語があります。

esolangs.org オリジナルの製作者 (UENO Katsuhiro さん) のサイトが2025年1月頃を最後に消えているので参照先が海外の Wiki しかない...

今回は Grass 言語に入門してみます。まずは Hello,world! を表示するのが基本の "き" ですね。

Grass 言語で Hello,world! に挑戦

Hello,world! を書くぞ!

wwwwWWWWwwWWWWwwwWwwwWWWwvwwwWWwWWWWwvwwWWwWWWwvwwWWwWWWwWWWWwvwwWWwWWWwWWWWwWWW
WWwWWWWWWwvwWWWWWWwwwwWwwwvWwWWWWwwwwwvwWWWWWWWWWwwwwwWwwwwwwwWWWWWWWWWWWwWwwwww
wwwwvWwvwWWWWWWWWWWWwwWwwwvWwvwWWWWWWWWWWWWwwwwwwwwwwwWwwwwwwwwwwwwWWWWWWWWWWWWW
WwWwwwwwwwwwwvWwvwWWWWWWWWWWWWwwwwwwwwwwwWWWWWWWWWWWWWWWWwWwwwwwwvWwvwWWwwwwwwww
wwwwwwwwwwwWwwwwwwwwwwwwwwwwwwwwwvwWWwWWWWWWWWWWWWWWWWWWWWWwWWWWWWWWWwwwwwwwwwww
wwwwwwwwwwwWwwvwWWwWWWWWWWWWWWwwwwwwwwwwwwwwwwwwwwwwWwwvwWWwWWWWWWWWwwwwwwwwwwww
wwwwwwwwwwwWwwvwWWwWWWWWWWWWwwwwwwwwwwwwwwwwwwwwwwwwWwwvwWWwWWWWWWWWWWWWWWWWWWWW
WWWWWwvwWWWwWWWWWWWWWWWWWWWWWwwwwwwwwwwwwwwwwwwwwwwwwwwWwwvwWWwWWWWWWWWWWWWWWWWW
WWWWWwwwwwwwwwwwwwwwwwwwwwwwwwwwWwwvwWWwWWWWWWWWWWWWWWWWWWWWWWWwwwwwwwwwwwwwwwww
wwwwwwwwwwwWwwvwWWWWWWWWWWWWWWWWWWWWWWWWWWWwvwWWWWWWWWwWWWwWWWWWWWWwWWWWWwWWWWWW
WWWwWWWWWWWwWWWWWWWWwWWWWWWWWWWWwWWWWWWWWWWwWWWWWWWWWWWWWWWWWWwWWWWWWWWWWWWwWWWW
WWWWWWWWWwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwWWWWWWWWWWWWWWwwwwWWWWWWWWWWWW
WWWWwWWWWWWWWWWWWWWWWwWWWWWWWWWWWWWWWWWwwwwwwwwwWWWWWWWWWWWWWWWWWWWWWWWwWWWWWWWW
WWWWWWWWWWWwWWWWWWWWWWWWWWWWWWWWWWWWWWWWwWWWWWWWWWWWWWWWWWWWWWwWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWwWWWWWWWWWWWWWWWWWWWWWWWw

よし書いた。

実行してみよう。

$ npx @susisu/grass-js helloworld.minify.grass 
Hello,world!

ヨシ! 👈⛑️

 

何がどうなっている?

これでは何もわからないと思うので、インデントやコメントを入れたバージョンを置いておきます。

// In
// _grass_
// Succ
// Out

// λm.λn.λf.λx.m f (n f x)
wwww WWWWww WWWWwww Wwww WWWw                                                   v // add
// λm.λ.n.λf.m (n f)
www WWw WWWWw                                                                   v // times

// λf.λx.f (f x)
ww WWw WWWw                                                                     v // 2
// λf.λx.f (f (f x))
ww WWw WWWw WWWWw                                                               v // 3
// λf.λx.f (f (f (f (f x))))
ww WWw WWWw WWWWw WWWWWw WWWWWWw                                                v // 5

w
  WWWWWW wwww /* add 2 */
  W      www  /* _   5 */                                                       v // const 7
Ww                                                                              v // 7
WWWW wwwww /* 3 2 = 2^3 => 8 */                                                 v // 8
w
  WWWWWWWWW   wwwww     /* add 5 */
  W           wwwwwww   /* _   3 */
  WWWWWWWWWWW w         /* add _ */
  W           wwwwwwwww /* _   3 */                                             v // const 11
Ww                                                                              v // 11
w
  WWWWWWWWWWW ww  /* add 11 */
  W           www /* _   11 */                                                  v // const 22
Ww                                                                              v // 22
w
  WWWWWWWWWWWW   wwwwwwwwwww  /* times 2 */
  W              wwwwwwwwwwww /* _     2 */
  WWWWWWWWWWWWWW w            /* times _ */
  W              wwwwwwwwww   /* _     7 */                                     v // const 28
Ww                                                                              v // 28
w
  WWWWWWWWWWWW     wwwwwwwwwww /* 3    5 */
  WWWWWWWWWWWWWWWW w           /* add  _ */
  W                wwwwww      /* _   22 */                                     v // const 147
Ww                                                                              v // 147

w
  WW wwwwwwwwwwwwwwwwwww   /* 147 Succ    */
  W  wwwwwwwwwwwwwwwwwwwww /* _   _grass_ */                                    v // const \n
w
  WW                      w                      /* \n              */
  WWWWWWWWWWWWWWWWWWWWW   w                      /* Succ \n   => VT */
  WWWWWWWWW               wwwwwwwwwwwwwwwwwwwwww /* 22   Succ       */
  W                       ww                     /* _    VT   => !  */          v // const !
w
  WW          w                      /* !            */
  WWWWWWWWWWW wwwwwwwwwwwwwwwwwwwwww /* 11 Succ      */
  W           ww                     /* _  !    => , */                         v // const ,
w
  WW       w                       /* ,            */
  WWWWWWWW wwwwwwwwwwwwwwwwwwwwwww /* 28 Succ      */
  W        ww                      /* _  ,    => H */                           v // const H
w
  WW          w                        /* H            */
  WWWWWWWWW   wwwwwwwwwwwwwwwwwwwwwwww /* 28 Succ      */
  W           ww                       /* _  H    => d */                       v // const d
w
  WW                        w /* d */
  WWWWWWWWWWWWWWWWWWWWWWWWW w /* Succ _ => e */                                 v // const e
w
  WWW               w                          /* d           */
  WWWWWWWWWWWWWWWWW wwwwwwwwwwwwwwwwwwwwwwwwww /* 8 Succ      */
  W                 ww                         /* _ d    => l */                v // const l
w
  WW                     w                           /* l           */
  WWWWWWWWWWWWWWWWWWWWWW wwwwwwwwwwwwwwwwwwwwwwwwwww /* 3 Succ      */
  W                 ww                               /* _ l    => o */          v // const o
w
  WW                      w                            /* o           */
  WWWWWWWWWWWWWWWWWWWWWWW wwwwwwwwwwwwwwwwwwwwwwwwwwww /* 3 Succ      */
  W                       ww                           /* _ o    => r */        v // const r

w
  WWWWWWWWWWWWWWWWWWWWWWWWWWW w /* Out _arg1_ */                                v // Out

// main
w
  WWWWWWWW                        w                                             /* H           */
  WWW                             w                                             /* Out H       */
  WWWWWWWW                        w                                             /* e           */
  WWWWW                           w                                             /* Out e       */
  WWWWWWWWW                       w                                             /* l           */
  WWWWWWW                         w                                             /* Out l       */
  WWWWWWWW                        w                                             /* Out l       */
  WWWWWWWWWWW                     w                                             /* o           */
  WWWWWWWWWW                      w                                             /* Out o       */
  WWWWWWWWWWWWWWWWWW              w                                             /* ,           */
  WWWWWWWWWWWW                    w                                             /* Out ,       */
  WWWWWWWWWWWWW                   wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww     /* Out _grass_ */
  WWWWWWWWWWWWWW                  wwww                                          /* Out o       */
  WWWWWWWWWWWWWWWW                w                                             /* r           */
  WWWWWWWWWWWWWWWW                w                                             /* Out r       */
  WWWWWWWWWWWWWWWWW               wwwwwwwww                                     /* Out l       */
  WWWWWWWWWWWWWWWWWWWWWWW         w                                             /* d           */
  WWWWWWWWWWWWWWWWWWW             w                                             /* Out d       */
  WWWWWWWWWWWWWWWWWWWWWWWWWWWW    w                                             /* !           */
  WWWWWWWWWWWWWWWWWWWWW           w                                             /* Out !       */
  WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW w                                             /* \n          */
  WWWWWWWWWWWWWWWWWWWWWWW         w                                             /* Out \n      */

解説は諦めます。

 

補足

今回書いたコードをここに置いておきます。

 

プログラムの実行には id:susisu さんの JavaScript 実装を使いました。npx で実行すればインストールも何も不要で便利。

github.com

 

Grass 言語の言語仕様や動作原理については公式サイトが詳しいです。
でも消えてしまっているので Wayback Machine などを漁る必要あり。

web.archive.org

 

まとめ

そろそろ Grass 言語を理解したいと思い立って勉強してみました。めちゃくちゃ大変だった...。

 

 

私からは以上です。

おまけ

そのほかの Hello,world! シリーズはこちら

blog.mudatobunka.org

combineではParserのタプルもまたParserになる

自然数の和と積を表現する構造体を考えよう。

#[derive(Debug, PartialEq)]
enum Term {
    Sum(usize, usize),
    Prod(usize, usize),
}

このような素朴な enum を用意する。
数式をパースして Term を得るようなパーザを作りたい。

combine というパーザコンビネータのライブラリがあるのでこれを使う。

docs.rs

自然数のパーザや演算子のパーザは1行で書ける。
自然数は '0' ~ '9' の文字の1回以上の連なりと定義できる。演算子は '+', '*' のいずれかだ。

use combine::parser::char::{Parser, one_of, parser::char::digit};

fn number<'a>() -> impl Parser<&'a str, Output = usize> {
    // 自然数は digit の1回以上の連なり
    // つづいて String としての "123" を usize としての 123 に変換する
    many1(digit()).map(|s: String| s.parse::<usize>().unwrap())
}

fn operator<'a>() -> impl Parser<&'a str, Output = char> {
    // 演算子は '+' か '*' のいずれか
    one_of("+*".chars())
}

これらを組み合わせて Term のパーザーを作ろう。

 

例えば文字列 "2*3" をパースするとしたら「number(), operator(), number() を立て続けに適用するパーザ」を作ればよさそうだ。
この「複数のパーザを立て続けに適用するパーザ」を表現したいときどう書けるか。

実はパーザをタプルで包むだけでいい。

fn term<'a>() -> impl Parser<&'a str, Output = (usize, char, usize)> {
    (number(), operator(), number())
}

たったこれだけ。 Parser<Outout=T1>, Parser<Outout=T2>, ..., Parser<Outout=Tn> のタプルは Parser<Output=(T1, T2, ..., Tn)> になる。

 

(number(), operator(), number()) 自体がパーザなので、いつものメソッドが使える。例えば .map() で結果を変換できる。

fn term<'a>() -> impl Parser<&'a str, Output = Term> {
    (number(), operator(), number())
        .map(|(lhs, op, rhs): (usize, char, usize)| match op {
            '+' => Term::Sum(lhs, rhs),
            '*' => Term::Prod(lhs, rhs),
            _ => unreachable!(),
        })
}

 

パーザが書けたので、実際にパースする処理を書くのは簡単。

fn parse_term(s: &str) -> Option<Term> {
    match term().parse(s) {
        Ok((term, _)) => Some(term),
        Err(_) => None,
    }
}

#[test]
fn test_parse_term() {
    assert_eq!(parse_term("12+34"), Some(Term::Sum(12, 34)));
    assert_eq!(parse_term("7*8"), Some(Term::Prod(7, 8)));
    assert_eq!(parse_term("7/8"), None);
}

いい感じ。

 

combine には .and() メソッドもある

「複数のパーザを立て続けに適用するパーザ」を表現するには他にも方法がある。

.and() メソッドを使えば、 p1.and(p2) のようにして p1, p2 を立て続けに適用するパーザを表現できる。

use combine::parser::char::char;

fn negative_number<'a>() -> impl Parser<&'a str, Output = isize> {
    char('-').and(number()).map(|(_, n)| -(n as isize))
}

p1: Parser<Output=T1>, p2: Parser<Output=T2> に対して p1.and(p2)Parser<Output=(T1, T2)> になる。
(p1, p2)p1.and(p2) は同じ振る舞いになる。

 

ところがパーザが3つ以上連なるときに .and() を使って合成しようとすると、結果は少しいびつなものになる。

fn term<'a>() -> impl Parser<&'a str, Output = ((usize, char), usize)> {
    number().and(operator()).and(number())
    // メソッドチェーンで綺麗に書ける
    // しかし結果は (usize, char, usize) ではなく ((usize, char), usize) になる
}

.and() は呼び出しごとに一組ずつタプルで包むので、結果は ((T1, T2), T3) になる。もし4つのパーザを合成しようとすれば、結果は (((T1, T2), T3), T4) になるはずだ。
タプルがネストしてしまうのは少々ツラい。

一方、パーザをタプルで書き並べる形なら、結果がフラットで読みやすい。

fn term<'a>() -> impl Parser<&'a str, Output = (usize, char, usize)> {
    (number(), operator(), number())
    // 結果は (usize, char, usize) になる
}

 

タプルに対してトレイトを impl する

タプルに対して何らかのトレイトを実装するのは Rust では一般的なことだ。
combine の実装をサラリと見ておこう。

combine/src/parser/sequence.rs

tuple_parser!(PartialState1; A);
tuple_parser!(PartialState2; A, B);
tuple_parser!(PartialState3; A, B, C);
// ...
tuple_parser!(PartialState20; A, B, C, D, E, F, G, H, I, J, K, L, M, N, P, Q, R, S, T, U);

1要素のタプルから20要素のタプルまで、定義がずらりと並んでいる。

 

タプルに対してトレイトを impl する (カスの応用)

私がお気に入りのトレイトである From トレイト を例にする。

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let num = Number::from(30);
    println!("My number is {:?}", num);
    // => My number is Number { value: 30 }
}

自作の構造体 Number に対して From<i32> トレイトを impl することで Number::from(42) が呼び出し可能になる。

From<i32> を impl するだけで、Into トレイトも使用可能になって、let num: Number = 42.into(); のように書ける。これも便利。

 

ところで From トレイトは1引数のメソッドしかサポートしていないのが不便だったりする。
そこでタプルに対して From トレイトを impl することで擬似的に多引数に対応できる。

impl From<(i32, i32)> for Number {
    fn from((m, n): (i32, i32)) -> Self {
        Number { value: m * n }
    }
}

fn main() {
    let num = Number::from((30, 40));
    println!("My number is {:?}", num);
    // => My number is Number { value: 1200 }
}

Number::from() がまるで2引数関数のように呼べるようになった。

 

まとめ

combine にて、タプルに対してトレイトを impl するテクニックの綺麗な応用が見られて感激。

 

 

私からは以上です。