Rust dojo第71回を開催した

第71回です。前回はこちら。

kdnakt.hatenablog.com

[第71回の様子]

2023/03/22に第71回を開催した。

内容としてはRust By Example 日本語版20. 標準ライブラリのその他の「20.1.1. Testcase: map-reduce」の演習問題に取り組んだ。先週全くやり方を思いつかなかったが、今週は何とか30分の枠内に収めることができた!
参加者は自分を入れて4人。前回の続きだったので、同じく自分がドライバを担当した。

[学んだこと]

  • 20.1.1. Testcase: map-reduce
  • 最後の課題に取り組んだ
    • 空白文字でユーザーの入力を区切り、それぞれのデータをスレッドで処理して処理結果を合計すると言うプログラムがある
    • スレッドの起動数がユーザー入力に依存している点を固定のスレッド数にせよとの課題
  • 普段扱っているJavaのイメージで、Executors.newFixedThreadPool(10);みたいなことができるのか?と思い標準ライブラリを調べてみた

doc.rust-lang.org

    • thread::Builder::new().name("thread1".to_string())で名前をつけることはできそう
    • だが、スレッドの起動数をコントロールする方法は記載されていなかった
  • 問題文をあらためて読み直してみると、「Modify the program so that the data is always chunked into a limited number of chunks」とある
    • データが常に一定数に分割されるように修正せよ、とあるので、分割方法を変えれば良いことに気がついた
  • ちょっと雑だが、空白文字で区切った後のチャンクを、一定数のチャンクにまとめ直す、と言う方針でいくことに
  • まずは文字列のベクタ(limited_chunks)を固定長で宣言する
use std::thread;

const NCHUNKS: u32 = 10;

fn main() {
    let data = "8696789 773741 64718 53297 32705036 ...(省略)";

    let chunked_data = data.split_whitespace();

    let mut limited_chunks = vec![String::new(); NCHUNKS];

    // Map処理
    
    // Reduce処理
    
    // 結果出力
}
  • ここに、chunked_dataのデータを順に詰め直していく
  • 最初は次のように実装したところ、コンパイルエラーがでた
const NCHUNKS: u32 = 10;

(中略)

let mut limited_chunks = vec![String::new(); NCHUNKS];
for (i, data_segment) in chunked_data.enumerate() {
    limited_chunks[i % NCHUNKS].push(data_segment);
}

// 以下のコンパイルエラー

error[E0308]: mismatched types
  --> src/main.rs:33:50
   |
33 |     let mut limited_chunks = vec![String::new(); NCHUNKS];
   |                              --------------------^^^^^^^-
   |                              |                   |
   |                              |                   expected `usize`, found `u32`
   |                              arguments to this function are incorrect
   |
    しかたがないので、NCHUNKSの型をusizeに修正する
  • 再度実行すると次のエラーがでた
error[E0308]: mismatched types
  --> src/main.rs:37:42
   |
37 |         limited_chunks[i % NCHUNKS].push(data_segment);
   |                                     ---- ^^^^^^^^^^^^ expected `char`, found `&str`
   |                                     |
   |                                     arguments to this method are incorrect
   |
  • 確認すると、String型に&strを追加するにはpush_str()を利用する必要があった

doc.rust-lang.org

  • push_str()を修正して、次のようにマップフェーズの処理と繋げて実行してみた
use std::thread;

const NCHUNKS: usize = 10;

fn main() {
    let data = "8696789 773741 64718 53297 32705036 ...(省略)";

    let chunked_data = data.split_whitespace();

    let mut limited_chunks = vec![String::new(); NCHUNKS];
    for (i, data_segment) in chunked_data.enumerate() {
        limited_chunks[i % NCHUNKS].push_str(data_segment);
    }
    // Map処理
    for (i, data_segment) in limited_chunks.enumerate() {
        // 省略
    }
    
    // Reduce処理
    
    // 結果出力
}

// 以下のコンパイルエラー
error[E0599]: `Vec<String>` is not an iterator
  --> src/main.rs:44:45
   |
44 |     for (i, data_segment) in limited_chunks.enumerate() {
   |                                             ^^^^^^^^^ `Vec<String>` is not an iterator; try calling `.into_iter()` or `.iter()`
  • ご丁寧にコンパイラ.into_iter()を呼んでイテレータにしてからenumerate()を実行しろと教えてくれた
  • 最終的に以下のような形になって、無事スレッドの起動数を制限することができた
use std::thread;

const NCHUNKS: usize = 10;

fn main() {
    let data = "8696789 773741 64718 53297 32705036 ...(省略)";

    let chunked_data = data.split_whitespace();

    let mut limited_chunks = vec![String::new(); NCHUNKS];
    for (i, data_segment) in chunked_data.enumerate() {
        limited_chunks[i % NCHUNKS].push_str(data_segment);
    }
    // Map処理
    for (i, data_segment) in limited_chunks.into_iter().enumerate() {
        // 省略
    }
    
    // Reduce処理
    
    // 結果出力
}
  • 今にして思えば、空白文字の出現位置によってスレッドに渡されるチャンクの長さが変わってきて効率が良くない
    • 空白文字を取り除いて、文字数で分割していけばよかった気がする...

[まとめ]

モブプログラミングスタイルでRust dojoを開催した。
効率的なプログラムを書くのって難しい...。

今週のプルリクエストはこちら。

github.com