Open163

Rust を ChatGPT だけで勉強する【実験記録】

ピン留めされたアイテム
Ryo SuzukiRyo Suzuki

C++ 経験者が、ChatGPT との対話だけで Rust を学んでみる実験の記録です。
期待した回答が得られなかった際には、質問文を変えたり、回答を再生成したりしています。
AI の回答には不正確な情報が含まれている可能性もあります。不正確な技術情報が拡散しないよう、AI の回答は画像で掲載しています。

指定した日へジャンプ

Ryo SuzukiRyo Suzuki

ここから 1 日目

ChatGPT と会話することで Rust を学んでみたいと思います。私は C++ 経験者ですが、Rust については聞いたことがあるだけで、文法もライブラリも知りません。まずは Rust の Hello world プログラムについて教えてください。

Ryo SuzukiRyo Suzuki

なぜ println! は関数ではなくマクロなのですか。また、末尾の ! は何を意味しますか?

Ryo SuzukiRyo Suzuki

Hello world のプログラムを見ると、モジュールのインポートを明示的に記述していませんが、それで良いのですか?

Ryo SuzukiRyo Suzuki
fn main() {
  println!("Hello, world!");
}

はコンパイルできましたが、

fn main() {
  std::io::println("Hello, world!");
}

は error[E0425]: cannot find function println in module std::io というエラーが出ました。なぜですか?

Ryo SuzukiRyo Suzuki

なるほど、Rust のマクロは C++ のマクロよりも、多くの仕事をするのですね。

Ryo SuzukiRyo Suzuki

その「標準ライブラリの関数を内部的に呼び出す」という機能に名前はついていますか?

Ryo SuzukiRyo Suzuki

println! は文字列を出力したあと改行を出力するのですか?

Ryo SuzukiRyo Suzuki

print! マクロは std::io::print() 関数に展開されるのでしょうか?

Ryo SuzukiRyo Suzuki

次は Rust で整数を標準出力する簡単なサンプルコードを教えてください。

Ryo SuzukiRyo Suzuki

let の代わりに int のような明示的な型を書くことはできますか?

Ryo SuzukiRyo Suzuki

int ではなく i32 なのですね。i32 のような Rust の基本型を全部教えてください。

(回答はここで途切れている)

Ryo SuzukiRyo Suzuki

上記以外に、C++ でいう char や wchar_t, char32_t のような文字型はありますか?

Ryo SuzukiRyo Suzuki

C++ の char のように 1 バイトで文字を表現する型はありますか?

Ryo SuzukiRyo Suzuki

わかりました。Rust で 2 つの整数を標準出力する簡単なサンプルコードを教えてください。

Ryo SuzukiRyo Suzuki

そのコードを

fn main() {
    let x = 10, y = 20;
    println!("x = {}, y = {}", x, y);
}

に変更するとエラーになりました。なぜですか?

Ryo SuzukiRyo Suzuki

なるほど、わかりました。main() 関数に戻り値はありますか?

Ryo SuzukiRyo Suzuki

C++ のように、main() 関数の return 0; は省略できるのですね。

Ryo SuzukiRyo Suzuki

次は Rust の標準入力について、短いサンプルコードで教えてください。

Ryo SuzukiRyo Suzuki

上記のコードは知らない機能が多く登場して難しいです。コードの 1~5行目を説明してください。

Ryo SuzukiRyo Suzuki

上記コードは整数を標準入力するだけなのに、すごく長いコードだと思います。もっと短く書けますか?

Ryo SuzukiRyo Suzuki

それはコメントを省いただけですね。明日はこの続きから勉強したいと思います。ありがとうございました。

Ryo SuzukiRyo Suzuki

1 日目はここまでです。
チャットのセッションは、その日の学習が終わったらリセットするため、毎日、前日の会話の記憶がない ChatGPT から教わることになります。

Ryo SuzukiRyo Suzuki

ここから 2 日目

私は ChatGPT と会話しながら Rust を学んでいて、今日はその 2 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。昨日は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について ChatGPT から教えてもらいました。今日はまず文字列について学びたいと思います。

(今気付いたが、4 つ目のコードはコンパイルエラーになるな。明日問い詰めよう)

Ryo SuzukiRyo Suzuki

&str 型の値を String 型に変換したいときは .to_string() を使うのですね。

Ryo SuzukiRyo Suzuki

&str 型は文字列リテラルを参照するということは、String 型を &str 型に変換することはできないということですね?

Ryo SuzukiRyo Suzuki

ということは .as_str() は文字列のコピーを作成するのですか?

Ryo SuzukiRyo Suzuki
fn main() {
    let mut s = "hello".to_string();
    let s_str = s.as_str();

    println!("{}", s_str);

    s = "world".to_string();

    println!("{}", s);
    println!("{}", s_str);
}

これは
error[E0506]: cannot assign to s because it is borrowed
というエラーになりました。どういう意味ですか?

続きは?

続きは?

Ryo SuzukiRyo Suzuki

なるほど、借用という概念のおかげで C++ の std::string_view よりも安全に使えるわけですね。

Ryo SuzukiRyo Suzuki

では、また最初のほうのサンプルコードに関する質問です。

fn main() {
  println!("Hello");
}

が OK で、

fn main() {
  let s = "Hello";
  println!(s);
}

だとコンパイルエラーになるのはなぜですか?

Ryo SuzukiRyo Suzuki

なるほど、わかりました。&str 型の文字列の長さを調べる方法を教えてください。

Ryo SuzukiRyo Suzuki

確かにコンパイルエラーになりました。usize 型の値と u64 型の値を比較できないのは厳しい制限ですね。

Ryo SuzukiRyo Suzuki

なるほど、勉強になりました。今日はここまでにして、明日は文字列に関する操作を勉強したいと思います。ありがとうございました。

Ryo SuzukiRyo Suzuki

2 日目はここまでです。
1 日目と同じく、チャットのセッションはここでリセットし、明日も前日の記憶が無い ChatGPT から Rust を教わります。

Ryo SuzukiRyo Suzuki

ここから 3 日目

私は ChatGPT と会話しながら Rust を学んでいて、今日はその 3 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について学び、2 日目は文字列の基本 (&str 型と String 型)について学びました。今日は文字列の操作について詳しく勉強していこうと思います。


向いていま?

Ryo SuzukiRyo Suzuki

わかりました。ありがとうございます。
文字列リテラルを String 型にする方法として、昨日は .to_string() を教えてもらいました。

let s = String::from("hello world");

let s = "hello world".to_string();

に違いはありますか?

Ryo SuzukiRyo Suzuki

わかりました。私は String::from() のほうが、String 型であることが明示されているのでわかりやすいかなと思いました。

Ryo SuzukiRyo Suzuki

わかりました。確かに

let s: str = "hello world";

ではエラーが発生しました。
では次の質問です。昨日は &str 型のメソッドとして .len() を教えてもらいました。これ以外のメソッドをリストアップしてください。

続きをお願いします。

続きをお願いします。


続きをお願いします。

続きをお願いします。


Ryo SuzukiRyo Suzuki

教えていただいたサンプルコードを動かして、実際に期待通りの結果になりました。&str 型は参照している文字列を変更するようなメソッドを持たないというのも、私の予想どおりでした。&str 型について、初心者が知っておくべきことは、ほかにありますか?

続きをお願いします。


Ryo SuzukiRyo Suzuki

ありがとうございます。次は String についての質問に移ります。String 型の値を作る方法として、これまで
・String::new();
・"Hello".to_string();
・String::from("Hello")
を学びました。ほかにはありますか? 例えば C++ の std::string(10, 'a') に相当する機能はありますか?


Ryo SuzukiRyo Suzuki

そこでの .repeat() は次のコードのように、String ではなく &str のメソッドですよね。

fn main() {
  let s1: &str = "abc";
  let s2 = s1.repeat(2);
  println!("{}", s2);
}

どのような関数宣言になっていますか?

続きをお願いします。

Ryo SuzukiRyo Suzuki

わかりました。ありがとうございます。

fn main() {
  let s1: String;
  let s2: String = String::new();
  println!("{}", s1);
  println!("{}", s2);
}


error[E0381]: borrow of possibly-uninitialized variable: s1
というエラーが出るのはなぜですか?

続きをお願いします。

Ryo SuzukiRyo Suzuki

でも

fn main() {
  let s1: String;
  s1 = String::new();
  println!("{}", s1);
}

は OK なので、宣言時に必ず初期値が必要というわけではないですよね。

Ryo SuzukiRyo Suzuki

確かに mut が無くても、1 回までは = による代入ができるみたいですね。

fn main() {
  let s1: String;
  s1 = String::new();
  s1 = String::new(); // 2 回目はエラー
  println!("{}", s1);
}

Ryo SuzukiRyo Suzuki

Java の final も、宣言時に初期値を与えないで、その後一度だけ値を代入できたような気がします。その点では C++ の const よりも Java の final に近い気がします。

続きをお願いします。

Ryo SuzukiRyo Suzuki

String でよく使われるメソッドを箇条書きで教えてください。

Ryo SuzukiRyo Suzuki
fn main() {
  let mut s = String::from("aaa");
  s.push_str(s);
  println!("{}", s);
}

はエラーになりました。.push_str(string) の引数の型は何ですか?


Ryo SuzukiRyo Suzuki

では、

fn main() {
  let mut s1 = String::from("aaa");
  let s2 = String::from("bbb");
  s1.push_str(s2);
  println!("{}", s1);
}

はどのように直すと良いですか?

Ryo SuzukiRyo Suzuki

おー、.as_str() は昨日学びました。こういう時に使うのですね。
次のように、C++ みたいに += 演算子で文字列を結合することもできるみたいです。

fn main() {
  let mut s1 = String::from("aaa");
  let s2 = String::from("bbb");
  let s3 = String::from("ccc");
  s1.push_str(s2.as_str());
  s1 += s3.as_str();
  println!("{}", s1);
}

.push_str() と += は同じですか?

続きをお願いします。

Ryo SuzukiRyo Suzuki

その += 演算子の説明は怪しい気がします。それぞれの関数宣言を教えてください。

(AI の答えを誘導してしまったか?)

Ryo SuzukiRyo Suzuki

わかりました。今日はここまでにします。明日は文字列の標準入力や、文字列を整数に変換する方法を学びたいと思います。ありがとうございました。

(あ! 時間切れ。最後の挨拶を伝えられず、ちょっと寂しい)

Ryo SuzukiRyo Suzuki

3 日目はここまでです。
いつも通り、明日も前日の記憶が無い ChatGPT から Rust を教わります。

Ryo SuzukiRyo Suzuki

ここから 4 日目

私は ChatGPT と会話しながら Rust を学んでいて、今日はその 4 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について学び、2 日目と 3 日目はは文字列 (&str 型と String 型)やそのメソッドについて学びました。文字列の標準入力から勉強していこうと思います。文字列の標準入力のサンプルをお願いします。

Ryo SuzukiRyo Suzuki

ありがとうございます。pop() しなかった場合、input には入力時の改行文字が含まれたままになるみたいですね。

use std::io;

fn main()
{
    let mut input = String::new();
    io::stdin().read_line(&mut input)
        .expect("エラー: 標準入力からの読み込みに失敗しました");
  
    print!("{}", input.len());
}

で Hello と入力すると 6 が出力されました。

Ryo SuzukiRyo Suzuki
fn main() {
  let mut input = String::new();
  std::io::stdin().read_line(&mut input)
      .expect("エラー: 標準入力からの読み込みに失敗しました");
  println!("{}", input);
}

と書けば use std::io; は不要でした。このコードに何か問題はありますか?

Ryo SuzukiRyo Suzuki

String もモジュールをインポートせずに使えるんですね。String のために use を書かなくて良いのですか?

Ryo SuzukiRyo Suzuki
let s_str : &str = &s;  
let s_str : &str = s.as_str();

は同じということですか?

Ryo SuzukiRyo Suzuki

わかりました。今後は簡単な &s を使おうと思います。
ところで、std::string モジュールのように、use しなくてもインポートされるモジュールを教えてください。

続きを説明して。

続きを説明して。

Ryo SuzukiRyo Suzuki

use std::vec; もデフォルトでインポートされるみたいですね。

Ryo SuzukiRyo Suzuki

わかりました。文字列の標準入力の話に戻ります。
io::stdin().read_line(&mut input)
の &mut はどういう意味ですか?

Ryo SuzukiRyo Suzuki

最初のサンプルで read_line のあとの .expect() を消して、

use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input);
    input.pop();
    println!("{}", input);
}

としたら、
unused std::result::Result that must be used
という警告が出ました。説明してください。

続きを説明して。

Ryo SuzukiRyo Suzuki

.expect() を書かないと警告が出るのは、安全ですが、やや面倒ですね。

続きを説明して。


続きを説明して。

Ryo SuzukiRyo Suzuki

なるほど、これは柔軟な機能ですね。
io::stdin().read_line(&mut input).unwrap();
でも警告を抑制できました。
ちなみに read_line() が Err() を返す状況はどのような例がありますか?

Ryo SuzukiRyo Suzuki

わかりました。では次は
Hello
world
という 2 行を標準入力で受け取り、String 型の変数 s1, s2 に格納する方法を教えてください。

Ryo SuzukiRyo Suzuki

Hello world
という 1 行を標準入力で受け取り、Hello を s1 に、world を s2 に格納する方法を教えてください。

続きを説明して。

続きを説明して。

Ryo SuzukiRyo Suzuki

ありがとうございます。

#include <iostream>

int main()
{
	int n;
	std::cin >> n;
	std::cout << n << '\n';
}

を Rust で書くとどうなりますか?

Ryo SuzukiRyo Suzuki

ということは

use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input)
        .expect("エラー: 標準入力からの読み込みに失敗しました");

    // 入力された文字列から改行文字を削除
    input.pop();

    // 入力された文字列を整数型に変換
    let n: i32 = input.trim().parse()
        .expect("エラー: 整数への変換に失敗しました");

    println!("{}", n);
}

input.pop(); は冗長ではないですか?

Ryo SuzukiRyo Suzuki
let s = "123";
let n = s.parse().unwrap();

これはさすがにコンパイルエラーですね。

Ryo SuzukiRyo Suzuki

半角空白で区切られた 2 つの整数を入力するとその和を出力するプログラムを作りました。どうでしょうか?

use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input)
        .expect("エラー: 標準入力からの読み込みに失敗しました");

    // 入力された文字列から改行を削除し半角空白で分割
    let v: Vec<&str> = input.trim().split(" ").collect();
    assert_eq!(v.len(), 2);

    let x: i32 = v[0].parse().unwrap();
    let y: i32 = v[1].parse().unwrap();

    println!("{}", (x + y));
}

Ryo SuzukiRyo Suzuki

なるほど、そのコードでも動きました。いろいろな機能がありますね。今日はここまでにします。明日は演算子を学ぼうと思います。ありがとうございました。

(今日の中の人はあっさりだな)

Ryo SuzukiRyo Suzuki

4 日目はここまでです。
いつも通りセッションをリセットして、明日も前日の記憶が無い ChatGPT から Rust を教わります。

Ryo SuzukiRyo Suzuki

ここから 5 日目

私は ChatGPT と会話しながら Rust を学んでいて、今日はその 5 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について学び、2~4 日目は文字列 (&str 型と String 型)やそのメソッドについて学びました。今日は Rust の基本的な演算子から学びたいと思います。


Ryo SuzukiRyo Suzuki

println!("{}", 3.5 % 3.0); のように浮動小数点数にも % が使えるみたいですね。

Ryo SuzukiRyo Suzuki

それはエラーになりました。
次の C++ コードを Rust のコードにしてください。

#include <iostream>
#include <cmath>

int main()
{
	double x = std::pow(2.0, 4.0);
	std::cout << x << '\n';
}

Ryo SuzukiRyo Suzuki

それもエラーです。C++ の std::pow に相当する Rust の機能を教えてください。

(この回答はひどい)

Ryo SuzukiRyo Suzuki

error[E0425]: cannot find function pow in module num
というエラーになります。

Ryo SuzukiRyo Suzuki

それはエラーになるし cmp は明らかに違うと思います。では平方根はどう計算しますか?

Ryo SuzukiRyo Suzuki

平方根はそれで計算できました。

fn main() {
    let x = f64::pow(2.0, 4.0);
    println!("{}", x);
}

はコンパイルエラーでした。

(あきらめよう・・・)

Ryo SuzukiRyo Suzuki

f64 の累乗を求める方法を教えてください。

(やっとわかった)

Ryo SuzukiRyo Suzuki

f64 が提供する数学関数をリストアップしてください。

続きを教えて。

続きを教えて。

Ryo SuzukiRyo Suzuki

ありがとうございます。できることが広がりました。

fn main() {
  println!("{}", f64::sqrt(2.0));
  println!("{}", f64::abs(-5.4));
  println!("{}", f64::fract(12.345));
  println!("{}", f64::round(3.4));
  println!("{}", f64::floor(3.6));
}

Ryo SuzukiRyo Suzuki
fn main() {
  let a = 10;
  let b = 0.1;
  println!("{}", a + b);
}

のコードで
error[E0277]: cannot add a float to an integer
というエラーが出て、

fn main() {
  let a : i32 = 10;
  let b : f64 = 0.1;
  println!("{}", a + b);
}

だと
error[E0277]: cannot add f64 to i32
というエラーが出ました。

let a = 10;let a : i32 = 10; の違いを教えてください。

続きを教えて。

Ryo SuzukiRyo Suzuki
fn main() {
  let a = 10;
  let b : i64 = a;
  println!("{}", a + b);
}

で a は何型ですか?

Ryo SuzukiRyo Suzuki

エラーメッセージから推測するに

fn main() {
  let a = 10;
  let b : i64 = a;
  let c : i32 = a;
}

で a は i64 型として型推論され、

fn main() {
  let a = 10;
  let b : i32 = a;
  let c : i64 = a;
}

で a は i32 型として型推論されることがわかりました。
最初 a は「整数型」とだけ決めておいて、その後の初回の使われ方に応じて i32 や i64 のような具体的な型に定まるみたいですね。

Ryo SuzukiRyo Suzuki

ChatGPT には伝わってないけど、まぁ良いでしょう。
Rust で型をキャストする方法を知りたいです。以下の C++ コードを Rust のコードにしてください。

int a = 50;
auto b = static_cast<double>(a);

Ryo SuzukiRyo Suzuki

ありがとうございます。できました。Rust の true / false は bool 型ですか?

Ryo SuzukiRyo Suzuki

OK です。Rust ではどのように関数を作りますか? 整数が偶数かどうかを判定する関数のサンプルコードを使って教えてください。


続きを教えて。

Ryo SuzukiRyo Suzuki

2 つの整数を受け取り、その大きいほうを返す関数を作ってください。

Ryo SuzukiRyo Suzuki
fn is_even(n: i32) -> bool {
    n % 2 == 0;
}

だとエラーになることについて説明してください。

続きは?


Ryo SuzukiRyo Suzuki

OK です。

fn 関数名(引数名: 引数の型) -> 戻り値の型 {
    関数の本体;
}

の形式で、戻り値の無い関数を作る場合「戻り値の型」の部分には何を記述できますか。


Ryo SuzukiRyo Suzuki
fn Add(a: i32, b: i32) -> i32 {
  a + b
}

このような関数を定義したら
warning: function Add should have a snake case name
という警告が出ました。説明してください。

Ryo SuzukiRyo Suzuki
fn add(a: i32, b: i32) -> i32 {
  a + b
}

fn add(a: i32, b: i32, c: i32) -> i32 {
  a + b
}

fn main() {
 println!("{}", add(10, 20));
 println!("{}", add(10, 20, 30));
}

これは
error[E0428]: the name add is defined multiple times
というエラーになりました。関数のオーバーロードはできないのですか?

(質問のコードで + c が抜けてたのをさらっと直すところは優秀)

Ryo SuzukiRyo Suzuki

なるほど、わかりました。C++ の関数テンプレートのようなことはできますか?

template <class T>
T add(T a, T b)
{
	return a + b;
}

こういうの。


Ryo SuzukiRyo Suzuki

i32 や f64 型にメソッドを追加できるのですね。
i32 型の標準のメソッドで、よく使われるものを教えてください。


Ryo SuzukiRyo Suzuki

.sqrt() は i32 にはありませんでしたが f64 にはありました。
i32 のメソッドをもっと教えてください。

続きを教えて。

Ryo SuzukiRyo Suzuki

i32 型の checked_add() メソッドのシグネチャを教えてください。

Ryo SuzukiRyo Suzuki
fn main() {
  let x : i32 = 10;
  let y = x.checked_add(3);
  let z = x.checked_add(i32::MAX);
}

で y と z を標準出力する方法を教えてください。


続きを教えて。

Ryo SuzukiRyo Suzuki

Rust の if は C++ と見た目が違いますね。


続きを教えて。


(コードと本文が反転しちゃった)

Ryo SuzukiRyo Suzuki

x が 0 以上 10 以下であるかを調べるコードはこれで良いですか?

fn main() {
  let x = 10;
  if 0 <= x && x <= 10 {
    println!("yes");
  }
}


Ryo SuzukiRyo Suzuki

2 点のマンハッタン距離を求める関数を作りました。添削してください。

fn manhattan_distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
    (x1 - x2).abs() + (y1- y2).abs()
}

fn main() {
    println!("{}", manhattan_distance(5.5, 4.4, 1.0, 2.0));
}

Ryo SuzukiRyo Suzuki

なるほど、ありがとうございます。タプルを気軽に使えるのは C++ より便利ですね。今日はここまでにします。明日はループについて学ぶ予定です。また明日よろしくお願いします。

Ryo SuzukiRyo Suzuki

5 日目はここまでです。
いつも通りセッションをリセットして、明日も前日の記憶が無い ChatGPT から Rust を教わります。

Ryo SuzukiRyo Suzuki

ここから 6 日目

私は ChatGPT と会話しながら Rust を学んでいて、今日はその 6 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について、2~4 日目は文字列やそのメソッドについて、5 日目は演算子と関数、if について学びました。今日は Rust におけるループを学びたいと思います。

Ryo SuzukiRyo Suzuki

ありがとうございます。

for i in 0..10 {
     println!("{}", i);
}

は 0 から 9 を出力しましたが、9 から 0 を出力する場合はどうすれば良いですか?

Ryo SuzukiRyo Suzuki

.. について説明してください。上記以外に使い方の例がありますか?


続きを教えて。

Ryo SuzukiRyo Suzuki

なるほど、

fn main() {
  let a = 1..10;
  let b = 1..=10;
  println!("{:?}", a);
  println!("{:?}", b);
}

で a, b はそれぞれ何型ですか?

Ryo SuzukiRyo Suzuki

for, while, loop では break; を使えますか? サンプルコードを書いてください。


Ryo SuzukiRyo Suzuki

わかりました。C++ とだいたい同じですね。二重ループの内部から break する方法はありますか?


Ryo SuzukiRyo Suzuki

なるほど、便利ですね。ラベルは ' を付ければよいのですか。ラベルの名前のルールについて教えてください。

Ryo SuzukiRyo Suzuki

ループにおける break の使い方は
・break;
・break ラベル;
のほかにありますか?

Ryo SuzukiRyo Suzuki

ループにおける continue の使い方は
・continue ;
・continue ラベル;
の 2 つ以外にありますか?

Ryo SuzukiRyo Suzuki

わかりました。整数の配列があって、その各要素を出力するコードを教えてください。

Ryo SuzukiRyo Suzuki

そのサンプルで、ループを使って配列の要素をすべて 2 倍にする場合はどうしますか?

Ryo SuzukiRyo Suzuki

固定長の配列のメソッドを教えてください。

続きを教えて。

Ryo SuzukiRyo Suzuki

わかりました。Vec の機能を学べるサンプルを作ってください。


メソッド一覧を教えて。

続きを教えてください。

続きを教えて。

Ryo SuzukiRyo Suzuki

Vec にある sort(), reverse() のようなメソッドをほかにも教えてください。

コードの続きを教えて。



コードの続きを教えて。

Ryo SuzukiRyo Suzuki

Vec の要素を降順にソートするサンプルを教えてください。


Ryo SuzukiRyo Suzuki

sort_by() の引数は C++ でいうラムダ式のようです。Rust では何と言いますか?

Ryo SuzukiRyo Suzuki

let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
の要素の合計、平均、最大値を求めるコードを書いてください。


Ryo SuzukiRyo Suzuki

最大値のところは * を付けて

fn main() {
    let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];

    // Vec の要素の合計値を求める
    let sum: i32 = numbers.iter().sum();
    println!("合計値: {}", sum);

    // Vec の要素の平均値を求める
    let avg: f64 = numbers.iter().sum::<i32>() as f64 / numbers.len() as f64;
    println!("平均値: {:.2}", avg);

    // Vec の要素の最大値を求める
    let max: i32 = *numbers.iter().max().unwrap();
    println!("最大値: {}", max);
}

とするとコンパイルできました。
numbers.iter().max() の max() の戻り値の型を教えてください。

Ryo SuzukiRyo Suzuki

指定した値と同じ要素の個数を調べる方法 (C++ でいう std::count()) を教えてください。

Ryo SuzukiRyo Suzuki

String で filter() や sort() を使う場合はどうすれば良いですか?


Ryo SuzukiRyo Suzuki

入力した文字列(v と w で構成される)に含まれる
・v の個数
・w の個数の倍
の合計を求めるプログラムを作りました。添削してください。

use std::io;

fn main() {
  let mut input = String::new();
  
  io::stdin().read_line(&mut input).unwrap();

  input.pop();
  
  let v_count = input.chars().filter(|c| *c == 'v').count();

  let w_count = input.len() - v_count;
  
  println!("{}", v_count + w_count * 2);
}

(AtCoder ABC 279 A に挑戦し、初めて Rust コードで AC(正解)になった。実際の提出)


続きを説明して。

続きを説明して。

Ryo SuzukiRyo Suzuki

trim() を使うのもありですよね。今日はありがとうございました。Rust でいろいろなプログラムが書けるようになってきました。明日は match について学びたいと思います。

Ryo SuzukiRyo Suzuki

6 日目はここまでです。
いつも通りセッションをリセットして、明日も前日の記憶が無い ChatGPT から Rust を教わります。

Ryo SuzukiRyo Suzuki

Rust を ChatGPT だけで勉強する【実験記録】6 日目までの感想

これは Rust Advent Calendar 2022 その 2 の 9 日目の記事です。

概要

日本時間 12 月 1 日に公開された ChatGPT を用いるプログラミング学習の可能性 を探るため、以前から興味を持っていた Rust を、書籍や Web の情報を参照せずに ChatGPT との対話だけで勉強する 試みを始め、本ページで今日まで 6 日間ログを記録してきました。

筆者について

筆者はメイン言語が C++ で、Java と Haskell を、実用的なコードは書けない程度触ったことがあります。また、早稲田大学・上智大学での C / C++ 講義、情報オリンピック日本代表選手への個人レッスン、C++ 入門書の商業出版など、プログラミング教育に携わっています。

これまでの状況

Rust を 1 行も書いたことがない状態からスタートし、4 日目には 「半角空白で区切られた 2 つの整数を入力するとその和を出力するプログラム」 を次のように自力で作成しました。

use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input)
        .expect("エラー: 標準入力からの読み込みに失敗しました");

    // 入力された文字列から改行を削除し半角空白で分割
    let v: Vec<&str> = input.trim().split(" ").collect();
    assert_eq!(v.len(), 2);

    let x: i32 = v[0].parse().unwrap();
    let y: i32 = v[1].parse().unwrap();

    println!("{}", (x + y));
}

6 日目には、AtCoder の初心者向けコンテスト AtCoder Beginner Contest 第 279 回の A 問題(コンテスト 1 問目の易しい問題)で 正解(AC) できました。以下の提出コードの意味もおおよそ把握できるほど、Rust コーディングに慣れてきました。

use std::io;

fn main() {
    let mut input = String::new();
  
    io::stdin().read_line(&mut input).unwrap();

    input.pop();
  
    let v_count = input.chars().filter(|c| *c == 'v').count();

    let w_count = input.len() - v_count;
  
    println!("{}", v_count + w_count * 2);
}

ただし、「やりたいことが書けるようになってきた」だけで、Rust として理想的なコードになっているかは、ChatGPT の「このプログラムは問題ないです」という言葉を信じるしかなく、不安は残ります。

Rust のコンパイラはかなり親切で、

  • 関数名はスネークケースで書く
  • この mut は不要

ということも教えてくれるため、筆者のコードのスタイルはそこまで悲惨なことにはなっていないと信じています。

勉強方法

毎日リセットされる ChatGPT を相手に Rust に関する質問を繰り返し、その回答をヒントにコードをオンラインコンパイラで動かしたり改造したりしながら Rust を学んできました。有用な回答が得られなかったり、回答があまりにも的外れだったりした場合は、質問文を変えたり、回答を再生成したりしています。ログを読む上ではかなりスムーズに応答できているように見えますが、実際は採用されなかった質問や回答が、掲載分の 3~5 倍は隠れている点に注意が必要です。作業時間は、記事化も含め毎日 2~3 時間前後です。

すでに多くの ChatGPT 利用者が指摘している通り、ChatGPT は不正確な情報であっても事実のように回答することがあります。今振り返ると、1 日目や 2 日目は結構騙されていた雰囲気があります。しかし、日を重ねるうちに Rust の世界観やコンパイラでの検証、ChatGPT への質問方法に慣れてきて、最近は状況証拠や推論を組み合わせながら真実を見つけていくという、推理ゲームに挑戦する感覚で取り組むようになってきました。

ChatGPT でプログラミング言語を勉強するメリット

この学習法で筆者が感じたメリットを挙げます。

  • 質問相手に気を遣わずに、いくらでも質問ができる
  • 質問の文脈が保存され、それに基づいた回答をしてくれる
    • ほとんどの質問で「Rust」と明示的に書いていない
    • 直前に ChatGPT が提示したサンプルコードに対して「その変数 a は…」と質問できる
  • 複雑なリクエストに沿ったサンプルコードを数秒で提示してくれる
    • さらに、各行の説明もしてくれる
    • 別のプログラミング言語からの翻訳も可能
    • コードを動かして理解するタイプの学習者にはかなり強力
  • ..& のような、既存の検索エンジンでは検索が困難な記号について質問できる
  • コードを添削してくれる
  • 同意、共感、応援のメッセージを送ってくれるので、報酬系が刺激される
    • 「そうです」「正しいです」「問題ありません」「明日からのプログラミング学習も頑張ってください」
  • 深さ優先の学習、幅優先の学習どちらにも適している
    • 興味を持った事柄をどこまでも掘り下げられる
    • 俯瞰的な情報を提示してくれる

質問をどこまでも掘り下げて行けるところや、いつでも以前の地点に戻って別の方向へ探索を進めることができる様子は、オープンワールドな学習 であるという感想を抱きました。

ChatGPT のデメリット

挑戦は続くため、AI の回答の答え合わせは当面行う予定はありません。また、筆者は Rust 初心者なので適切に評価することも困難です。ChatGPT の回答の正確性や課題についての考察は、Rust の専門家に任せたいと思います。それ以外の面でいうと、挑戦している間、少なくとも次のような傾向が感じられました。

  • ChatGPT 自身が知らないことを「知らない」と答えない
  • 過去に回答した内容を、その後の回答でも一貫させることに固執しすぎる
    • 定期的にスレッドをリセットして、マルチオピニオンで勉強したほうが良い
    • ChatGPT の中には知識や癖の異なる何万人もの回答者がいて、何回か質問や回答の選択を繰り返すことで、担当者が 1 人に収束し、それ以降はその担当者の範疇でしか回答をもらえなくなるような感覚があった
  • 美学を持たない
    • 複数の選択肢があるコードについて、とくにこだわりを持たずにランダムなもの、あるいは直近の文脈に近いものを提示してくる
    • 良い意味でも悪い意味でも「好み」を持たないため、コードがちぐはぐに感じることがある

プログラミング学習に ChatGPT を導入することを考えている方は、少なくともこうした点を知っておくと、良い付き合い方ができると思います。

おわりに

「C++ 経験者が Rust を学ぶ」という限定的なケースではありますが、登場したばかりの ChatGPT の雰囲気や実力を垣間見れる十分まとまった記録になっていると思います。また、知人からは 筆者の第二プログラミング言語の学習法というメタ的な知見 を学べる点が興味深いというコメントをもらっています。本記録を通して、ChatGPT に代表される言語モデルの活用法やプログラミング教育の未来、自身のプログラミング学習について考えるきっかけになれば幸いです。

画像で掲載している ChatGPT の応答は、非公開のリポジトリにテキスト形式でもバックアップしています。研究目的等でテキストデータを必要とする方はお気軽にご相談ください。