🤖

ChatGPTにRustの課題をだしてもらい、GitHub Copilotと解いていく Day 7

2023/04/22に公開

この記事は?

Rust初学者のわたしが、ChatGPTさんに課題を出してもらって、GitHub Copilotさんと一緒にペアプロしながら学習しているものです。

基本的には

  • ChatGPTこと「先生」に課題を出してもらう。
  • それについて私がどういう風にコードを書いていくか考えてみる
  • わからないことや壁打ちをChatGPTにしてみる
  • 方針が固まったらGitHub Copilotさんにコメントで方針を示しながら書いていく
  • 動くものができたらChatGPTさんに提出
    というながれで作業しています。

(Rubyの経験が少しあるので、ほとんど頭の中はRubyコードで思考していることが多いです。)

今回の課題は「テキストファイル内の単語の出現回数を数えるプログラムを作成してください。」というものです。

Day 7

■私
着手は明日になってしまうのですが、新しい課題をお願いします!

■先生
もちろんです!次の課題をご紹介します。

課題7:テキストファイル内の単語の出現回数を数えるプログラムを作成してください。

プログラムの概要:

テキストファイルを読み込む
単語の出現回数をカウントする
出現回数が最も多い単語とその回数を表示する
詳細:

コマンドライン引数でテキストファイル名を指定します。
単語の区切りは空白文字(スペース、タブ、改行)とします。
単語の大文字と小文字は区別しないようにしてください(例:「apple」と「Apple」は同じ単語としてカウント)。
例えば、以下のようなテキストファイルが与えられたとき、

This is a sample text.
This text is just a sample.
プログラムは、「sample」および「is」が2回ずつ出現することを示すべきです。

この課題では、テキストファイルの読み込みや文字列操作、ハッシュマップの使用など、いくつかのスキルが必要になります。お時間があるときに着手してみてください。もし質問があれば、いつでもお気軽にお聞きください。

■私
質問です!
上記の課題について今まで取り組んでいないポイントとして、カウントをどういう仕組でやっていくかというところがポイントになるかと思いました。
直感的には以下のような流れをイメージしました。

  • テキストファイルを読み込む
  • ラインごとに読み込むか、いっそのこと全部読み込んでスペース・タブ・改行で区切った一次配列にしてしまう(Rustだと可変のVec?)& すべて小文字(もしくは大文字)に変換する
  • 単語と出現回数を記録できるRubyでいうところのハッシュかクラス(Rustだと構造体を使うのがいい?)
  • 配列をすべて処理して、出現回数を降順でソートしたときの最大出現数に該当する単語と回数を出力する

というものをイメージしました。
質問1:上記の流れは違和感がないか、または、考慮漏れがないか。
質問2:出現回数をカウントするデータにおすすめはあるか。

こちらについて教えて下さい!

■先生
質問1について:上記の流れは基本的に問題ありません。良いアプローチだと思います。

質問2について:Rustで出現回数をカウントするデータ構造として、std::collections::HashMapがおすすめです。単語をキーとして、出現回数を値とするハッシュマップを使用することで、単語の出現回数を効率的にカウントできます。

上記の流れに基づいて、以下の手順でプログラムを実装してみてください:

テキストファイルを読み込む
テキストファイルの内容をすべて小文字に変換する(to_lowercase()メソッドを使用)
内容をスペース・タブ・改行で区切ってVecに格納する
空のハッシュマップ(HashMap<String, u32>)を作成する
Vecに格納された単語を順番に処理し、ハッシュマップに単語と出現回数を記録する
ハッシュマップを降順にソートし、最大出現回数の単語とその回数を出力する
このアプローチで実装を進めてみてください。もし質問があればお気軽にどうぞ!

■私
すみません、仕事が立て込んでおりご無沙汰しました。ちょっと今日取り組んでみようと思います。

■先生
大丈夫です、お忙しい中お時間を割いてくださりありがとうございます。もし質問や相談があればお気軽にどうぞ!頑張ってください!

■私
質問です! Stringを特定文字列で分割するのにsplitを使用しようとしているのですが、複数の文字で分割するのはどのように引数を指定すればよいでしょうか?
Rustでは、split関数は一つのパターンしか受け付けません。ですので、複数の文字で分割するには、splitをチェインして使用するか、splitを引数として受け取る関数(例えば、split_inclusive)を使用することができます。また、正規表現を使う方法もあります。

チェインしてsplitを使用する方法:

let text = "hello, world! how are you?";
let words: Vec<&str> = text
.split(|c| c == ',' || c == '!' || c == '?')
.flat_map(|s| s.split_whitespace())
.collect();

正規表現を使用する方法:
まず、regexクレートを使うために Cargo.toml に依存関係を追加します。

[dependencies]
regex = "1.0"

そして、次のようにコードを書くことができます。

use regex::Regex;

fn main() {
let text = "hello, world! how are you?";
let re = Regex::new(r"[,!?]").unwrap();
let words: Vec<&str> = re.split(text).flat_map(|s| s.split_whitespace()).collect();

    println!("{:?}", words);
}

どちらの方法でも、複数の区切り文字で文字列を分割することができます。どの方法を選ぶかは、パフォーマンスや好みによって異なります。パフォーマンスが重要でなければ、正規表現を使う方法は柔軟性があり便利です。

■私
素朴な疑問なのですが、課題7についてサンプルのテキストファイルを渡したとき「プログラムは、「sample」および「is」が2回ずつ出現することを示すべきです。」と書かれていますが、もしかして「a」もこれに含まれますか?

■先生
おっしゃる通り、「a」も2回出現していますね。すみません、その点は見落としていました。課題7の指示に従って実装すれば、実際には「sample」、「is」、そして「a」がそれぞれ2回ずつ出現することが示されるはずです。説明に齟齬があって申し訳ありません。

■私
実装しました!

use std::fs::File;
use std::io::Read;
use std::collections::HashMap;

fn main() {
    // コマンドライン引数からファイルネームを取得
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.len() < 1 {
        println!("引数が足りません");
        return;
    }

    // ファイルを開く
    let mut filename = args[0].clone();
    let mut file = File::open(filename).expect("ファイルを開けません");
    let mut contents = String::new();
    file.read_to_string(&mut contents).expect("ファイルの読み込みに失敗しました");

    // 読み込んだファイルの内容を「スペース」「改行」「タブ」で区切った単語を格納するVecを作成する
    let binding = contents.to_lowercase();
    let words: Vec<&str> = binding.split(|c| c == ' ' || c == '\n' || c == '\t').collect();

    // 出現した単語の出現数をカウントするためのHashMapを作成する
    // 単語をカウントしつつ単語と出現数をHashMapに格納する
    let mut word_count: HashMap<&str, usize> = HashMap::new();
    for word in words  {
        if word_count.contains_key(word) {
            let count = word_count.get_mut(word).unwrap();
            *count += 1;
        } else {
            word_count.insert(word, 1);
        }
    }

    // HashMapの単語の出現数を降順にソートし、最も多い出現数の単語と出現数を表示する
    let mut sorted_word_count: Vec<(&&str, &usize)> = word_count.iter().collect();
    sorted_word_count.sort_by(|a, b| b.1.cmp(a.1));

    let highest_number = sorted_word_count[0].1;
    for word_set in sorted_word_count {
        if word_set.1 == highest_number {
            println!("単語: {}, 出現数: {}", word_set.0, word_set.1);

        }
    }
}

確認をお願いします!
(ちなみに先程の指摘は自分のプログラムを動かしてみたことで気づきました。なので自分も見落としていました。)

■先生
実装お疲れ様です!コードを確認しましたが、非常に良いと思います。順序立ててコードを書かれており、読みやすく、わかりやすいです。また、出現回数の最も多い単語とその回数を出力するための処理もうまく書かれています。

1点だけコメントがあります。

コード内でコメントにある「スペース」「改行」「タブ」に加えて、句読点やその他の特殊文字(例えばカンマ、ピリオドなど)を除去する処理が追加されると、より正確な結果が得られます。ただし、これは完全に必須ではなく、余裕があれば取り組んでみてください。

それ以外は問題ありませんので、このままでも大丈夫です!次の課題に進んでいただいても構いません。

■私

指摘の対応をしてみました。するとご指摘の通り 「.」「,」が含まれていないことで、samplesample.が適切に処理できてないことに気が付きました。
また「.」でsplitした時に、空の文字列がHashMapに追加されていることに気が付きました。その部分も踏まえ以下のように修正をしています

use std::fs::File;
use std::io::Read;
use std::collections::HashMap;

fn main() {
    // コマンドライン引数からファイルネームを取得
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.len() < 1 {
        println!("引数が足りません");
        return;
    }

    // ファイルを開く
    let mut filename = args[0].clone();
    let mut file = File::open(filename).expect("ファイルを開けません");
    let mut contents = String::new();
    file.read_to_string(&mut contents).expect("ファイルの読み込みに失敗しました");

    // 読み込んだファイルの内容を「スペース」「改行」「タブ」で区切った単語を格納するVecを作成する
    let binding = contents.to_lowercase();
    let words: Vec<&str> = binding.split(|c| c == '.' || c == ' ' || c == '\n' || c == '\t' || c == ',').collect();

    // 出現した単語の出現数をカウントするためのHashMapを作成する
    // 単語をカウントしつつ単語と出現数をHashMapに格納する
    let mut word_count: HashMap<&str, usize> = HashMap::new();
    for word in words  {
        if word_count.contains_key(word) {
            let count = word_count.get_mut(word).unwrap();
            *count += 1;
        } else if word == "" {
            continue;
        } else {
            word_count.insert(word, 1);
        }
    }

    // HashMapの単語の出現数を降順にソートし、最も多い出現数の単語と出現数を表示する
    let mut sorted_word_count: Vec<(&&str, &usize)> = word_count.iter().collect();
    sorted_word_count.sort_by(|a, b| b.1.cmp(a.1));

    let highest_number = sorted_word_count[0].1;
    for word_set in sorted_word_count {
        if word_set.1 == highest_number {
            println!("単語: {}, 出現数: {}", word_set.0, word_set.1);
        }
    }
}

ご確認お願いいたします。

■先生
修正されたコードを確認しました。句読点やその他の特殊文字(カンマやピリオド)の除去、そして空の文字列がHashMapに追加されないようにする処理が追加されており、問題ないと思います。コードも読みやすく、理解しやすいです。

現在の実装でより正確な結果が得られるようになりましたので、このままでも大丈夫です。お疲れ様でした!次の課題に進んでいただいて構いません。

Discussion