Rust を ChatGPT だけで勉強する【実験記録】
C++ 経験者が、ChatGPT との対話だけで Rust を学んでみる実験の記録です。
期待した回答が得られなかった際には、質問文を変えたり、回答を再生成したりしています。
AI の回答には不正確な情報が含まれている可能性もあります。不正確な技術情報が拡散しないよう、AI の回答は画像で掲載しています。
- ChatGPT: https://chat.openai.com/chat
- オンラインコンパイラ: https://replit.com/languages/rust
指定した日へジャンプ
ここから 1 日目
ChatGPT と会話することで Rust を学んでみたいと思います。私は C++ 経験者ですが、Rust については聞いたことがあるだけで、文法もライブラリも知りません。まずは Rust の Hello world プログラムについて教えてください。
なぜ println! は関数ではなくマクロなのですか。また、末尾の ! は何を意味しますか?
println!("Hello, world!"); はどのような関数の呼び出しに置き換えられますか?
Hello world のプログラムを見ると、モジュールのインポートを明示的に記述していませんが、それで良いのですか?
fn main() {
println!("Hello, world!");
}
はコンパイルできましたが、
fn main() {
std::io::println("Hello, world!");
}
は error[E0425]: cannot find function println
in module std::io
というエラーが出ました。なぜですか?
なるほど、Rust のマクロは C++ のマクロよりも、多くの仕事をするのですね。
その「標準ライブラリの関数を内部的に呼び出す」という機能に名前はついていますか?
マクロでない関数に ! を付けることはありますか?
println! は文字列を出力したあと改行を出力するのですか?
print! マクロは std::io::print() 関数に展開されるのでしょうか?
次は Rust で整数を標準出力する簡単なサンプルコードを教えてください。
let の代わりに int のような明示的な型を書くことはできますか?
int ではなく i32 なのですね。i32 のような Rust の基本型を全部教えてください。
(回答はここで途切れている)
上記以外に、C++ でいう char や wchar_t, char32_t のような文字型はありますか?
C++ の char のように 1 バイトで文字を表現する型はありますか?
str 型は UTF-32 ですか?
str 型は u8 型の配列ですか?
println!("x = {}", x); はどのように展開されますか?
format_args!("x = {}", x) はどのように展開されますか?
わかりました。Rust で 2 つの整数を標準出力する簡単なサンプルコードを教えてください。
そのコードを
fn main() {
let x = 10, y = 20;
println!("x = {}, y = {}", x, y);
}
に変更するとエラーになりました。なぜですか?
なるほど、わかりました。main() 関数に戻り値はありますか?
C++ のように、main() 関数の return 0; は省略できるのですね。
Rust のコメントは //
と /*
~ */
で、C++ と同じですか?
次は Rust の標準入力について、短いサンプルコードで教えてください。
上記のコードは知らない機能が多く登場して難しいです。コードの 1~5行目を説明してください。
上記コードは整数を標準入力するだけなのに、すごく長いコードだと思います。もっと短く書けますか?
それはコメントを省いただけですね。明日はこの続きから勉強したいと思います。ありがとうございました。
1 日目はここまでです。
チャットのセッションは、その日の学習が終わったらリセットするため、毎日、前日の会話の記憶がない ChatGPT から教わることになります。
ここから 2 日目
私は ChatGPT と会話しながら Rust を学んでいて、今日はその 2 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。昨日は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について ChatGPT から教えてもらいました。今日はまず文字列について学びたいと思います。
(今気付いたが、4 つ目のコードはコンパイルエラーになるな。明日問い詰めよう)
文字列リテラルは何型ですか?
最初のコードの s
は何型ですか?
&str 型の値を String 型に変換したいときは .to_string() を使うのですね。
&str 型は文字列リテラルを参照するということは、String 型を &str 型に変換することはできないということですね?
.as_str()
は何をするのですか?
.as_str() で s を &str 型の値に変換した後に s を変更したら困りません?
ということは .as_str() は文字列のコピーを作成するのですか?
C++ の string_view みたいなものですかね?
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
というエラーになりました。どういう意味ですか?
続きは?
続きは?
なるほど、借用という概念のおかげで C++ の std::string_view よりも安全に使えるわけですね。
では、また最初のほうのサンプルコードに関する質問です。
fn main() {
println!("Hello");
}
が OK で、
fn main() {
let s = "Hello";
println!(s);
}
だとコンパイルエラーになるのはなぜですか?
なるほど、わかりました。&str 型の文字列の長さを調べる方法を教えてください。
その .len() メソッドの戻り値の型は何ですか?
昨日教えてもらった Rust の基本型一覧によると、u32, u64, usize はそれぞれ異なる型なのですよね。それとも usize はどちらかの型エイリアスですか?
続きを説明して。
確かにコンパイルエラーになりました。usize 型の値と u64 型の値を比較できないのは厳しい制限ですね。
なるほど、勉強になりました。今日はここまでにして、明日は文字列に関する操作を勉強したいと思います。ありがとうございました。
2 日目はここまでです。
1 日目と同じく、チャットのセッションはここでリセットし、明日も前日の記憶が無い ChatGPT から Rust を教わります。
ここから 3 日目
私は ChatGPT と会話しながら Rust を学んでいて、今日はその 3 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について学び、2 日目は文字列の基本 (&str 型と String 型)について学びました。今日は文字列の操作について詳しく勉強していこうと思います。
向いていま?
わかりました。ありがとうございます。
文字列リテラルを String 型にする方法として、昨日は .to_string() を教えてもらいました。
let s = String::from("hello world");
と
let s = "hello world".to_string();
に違いはありますか?
わかりました。私は String::from() のほうが、String 型であることが明示されているのでわかりやすいかなと思いました。
&str 型の & は参照の記号ですか?
&str 型ではなく str 型の変数を宣言することはできますか?
続きをお願いします。
わかりました。確かに
let s: str = "hello world";
ではエラーが発生しました。
では次の質問です。昨日は &str 型のメソッドとして .len()
を教えてもらいました。これ以外のメソッドをリストアップしてください。
続きをお願いします。
続きをお願いします。
続きをお願いします。
続きをお願いします。
教えていただいたサンプルコードを動かして、実際に期待通りの結果になりました。&str 型は参照している文字列を変更するようなメソッドを持たないというのも、私の予想どおりでした。&str 型について、初心者が知っておくべきことは、ほかにありますか?
続きをお願いします。
ありがとうございます。次は String についての質問に移ります。String 型の値を作る方法として、これまで
・String::new();
・"Hello".to_string();
・String::from("Hello")
を学びました。ほかにはありますか? 例えば C++ の std::string(10, 'a') に相当する機能はありますか?
そこでの .repeat()
は次のコードのように、String ではなく &str のメソッドですよね。
fn main() {
let s1: &str = "abc";
let s2 = s1.repeat(2);
println!("{}", s2);
}
どのような関数宣言になっていますか?
続きをお願いします。
わかりました。ありがとうございます。
fn main() {
let s1: String;
let s2: String = String::new();
println!("{}", s1);
println!("{}", s2);
}
で
error[E0381]: borrow of possibly-uninitialized variable: s1
というエラーが出るのはなぜですか?
続きをお願いします。
でも
fn main() {
let s1: String;
s1 = String::new();
println!("{}", s1);
}
は OK なので、宣言時に必ず初期値が必要というわけではないですよね。
確かに mut が無くても、1 回までは = による代入ができるみたいですね。
fn main() {
let s1: String;
s1 = String::new();
s1 = String::new(); // 2 回目はエラー
println!("{}", s1);
}
Java の final も、宣言時に初期値を与えないで、その後一度だけ値を代入できたような気がします。その点では C++ の const よりも Java の final に近い気がします。
続きをお願いします。
String でよく使われるメソッドを箇条書きで教えてください。
fn main() {
let mut s = String::from("aaa");
s.push_str(s);
println!("{}", s);
}
はエラーになりました。.push_str(string)
の引数の型は何ですか?
では、
fn main() {
let mut s1 = String::from("aaa");
let s2 = String::from("bbb");
s1.push_str(s2);
println!("{}", s1);
}
はどのように直すと良いですか?
おー、.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()
と += は同じですか?
続きをお願いします。
その += 演算子の説明は怪しい気がします。それぞれの関数宣言を教えてください。
(AI の答えを誘導してしまったか?)
わかりました。今日はここまでにします。明日は文字列の標準入力や、文字列を整数に変換する方法を学びたいと思います。ありがとうございました。
(あ! 時間切れ。最後の挨拶を伝えられず、ちょっと寂しい)
3 日目はここまでです。
いつも通り、明日も前日の記憶が無い ChatGPT から Rust を教わります。
ここから 4 日目
私は ChatGPT と会話しながら Rust を学んでいて、今日はその 4 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について学び、2 日目と 3 日目はは文字列 (&str 型と String 型)やそのメソッドについて学びました。文字列の標準入力から勉強していこうと思います。文字列の標準入力のサンプルをお願いします。
ありがとうございます。pop() しなかった場合、input には入力時の改行文字が含まれたままになるみたいですね。
use std::io;
fn main()
{
let mut input = String::new();
io::stdin().read_line(&mut input)
.expect("エラー: 標準入力からの読み込みに失敗しました");
print!("{}", input.len());
}
で Hello と入力すると 6 が出力されました。
1 日目は trim() を使ったコードも教えてもらいました。標準入力から改行文字を削除する場合、pop() と trim() どちらを使うのが一般的ですか?
わかりました。ところで、io::stdin() を使うには use std::io が必要なのですね。
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input)
.expect("エラー: 標準入力からの読み込みに失敗しました");
println!("{}", input);
}
と書けば use std::io; は不要でした。このコードに何か問題はありますか?
String もモジュールをインポートせずに使えるんですね。String のために use を書かなくて良いのですか?
おや、 s + &s2
と s + s2.as_str()
に違いはありますか?
let s_str : &str = &s;
let s_str : &str = s.as_str();
は同じということですか?
わかりました。今後は簡単な &s を使おうと思います。
ところで、std::string モジュールのように、use しなくてもインポートされるモジュールを教えてください。
続きを説明して。
続きを説明して。
昨日のサンプルで登場した Vec も標準ライブラリの機能ですか?何というモジュールでしょう?
use std::vec; もデフォルトでインポートされるみたいですね。
わかりました。文字列の標準入力の話に戻ります。
io::stdin().read_line(&mut input)
の &mut はどういう意味ですか?
std::io::stdin().read_line() のシグネチャはどうなっていますか。
続きをお願いします。
最初のサンプルで 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
という警告が出ました。説明してください。
続きを説明して。
.expect() を書かないと警告が出るのは、安全ですが、やや面倒ですね。
続きを説明して。
続きを説明して。
なるほど、これは柔軟な機能ですね。
io::stdin().read_line(&mut input).unwrap();
でも警告を抑制できました。
ちなみに read_line() が Err() を返す状況はどのような例がありますか?
わかりました。では次は
Hello
world
という 2 行を標準入力で受け取り、String 型の変数 s1, s2 に格納する方法を教えてください。
Hello world
という 1 行を標準入力で受け取り、Hello を s1 に、world を s2 に格納する方法を教えてください。
続きを説明して。
続きを説明して。
ありがとうございます。
#include <iostream>
int main()
{
int n;
std::cin >> n;
std::cout << n << '\n';
}
を Rust で書くとどうなりますか?
trim() は改行文字も削除するみたいですが?
ということは
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();
は冗長ではないですか?
.parse()
のシグネチャを教えてください。
i64
型への変換を行う場合は?
f32
型への変換を行う場合は?
parse() では、便利な型推論が働いてくれるんですね。
let s = "123";
let n = s.parse().unwrap();
これはさすがにコンパイルエラーですね。
半角空白で区切られた 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));
}
改善案はありますか?
(文中のリンク先は https://doc.rust-lang.org/std/result/enum.Result.html) (ただし、ChatGPT だけで勉強するルールなので読まない)
続きを説明して。
なるほど、そのコードでも動きました。いろいろな機能がありますね。今日はここまでにします。明日は演算子を学ぼうと思います。ありがとうございました。
(今日の中の人はあっさりだな)
4 日目はここまでです。
いつも通りセッションをリセットして、明日も前日の記憶が無い ChatGPT から Rust を教わります。
ここから 5 日目
私は ChatGPT と会話しながら Rust を学んでいて、今日はその 5 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について学び、2~4 日目は文字列 (&str 型と String 型)やそのメソッドについて学びました。今日は Rust の基本的な演算子から学びたいと思います。
println!("{}", 3.5 % 3.0); のように浮動小数点数にも % が使えるみたいですね。
べき乗の演算子はありますか?
それはエラーになりました。
次の C++ コードを Rust のコードにしてください。
#include <iostream>
#include <cmath>
int main()
{
double x = std::pow(2.0, 4.0);
std::cout << x << '\n';
}
それもエラーです。C++ の std::pow に相当する Rust の機能を教えてください。
(この回答はひどい)
error[E0425]: cannot find function pow
in module num
というエラーになります。
それはエラーになるし cmp は明らかに違うと思います。では平方根はどう計算しますか?
平方根はそれで計算できました。
fn main() {
let x = f64::pow(2.0, 4.0);
println!("{}", x);
}
はコンパイルエラーでした。
(あきらめよう・・・)
Rust の三角関数のサンプルをお願いします。
続きを説明して。
f64 の指数を求める方法を教えてください。
f64 の累乗を求める方法を教えてください。
(やっとわかった)
f64 が提供する数学関数をリストアップしてください。
続きを教えて。
続きを教えて。
ありがとうございます。できることが広がりました。
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));
}
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;
の違いを教えてください。
続きを教えて。
fn main() {
let a = 10;
let b : i64 = a;
println!("{}", a + b);
}
で a は何型ですか?
エラーメッセージから推測するに
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 のような具体的な型に定まるみたいですね。
ChatGPT には伝わってないけど、まぁ良いでしょう。
Rust で型をキャストする方法を知りたいです。以下の C++ コードを Rust のコードにしてください。
int a = 50;
auto b = static_cast<double>(a);
ありがとうございます。できました。Rust の true / false は bool 型ですか?
OK です。Rust ではどのように関数を作りますか? 整数が偶数かどうかを判定する関数のサンプルコードを使って教えてください。
続きを教えて。
2 つの整数を受け取り、その大きいほうを返す関数を作ってください。
fn is_even(n: i32) -> bool {
n % 2 == 0;
}
だとエラーになることについて説明してください。
続きは?
println! に戻り値はありますか?
OK です。
fn 関数名(引数名: 引数の型) -> 戻り値の型 {
関数の本体;
}
の形式で、戻り値の無い関数を作る場合「戻り値の型」の部分には何を記述できますか。
fn Add(a: i32, b: i32) -> i32 {
a + b
}
このような関数を定義したら
warning: function Add
should have a snake case name
という警告が出ました。説明してください。
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
が抜けてたのをさらっと直すところは優秀)
なるほど、わかりました。C++ の関数テンプレートのようなことはできますか?
template <class T>
T add(T a, T b)
{
return a + b;
}
こういうの。
i32 や f64 型にメソッドを追加できるのですね。
i32 型の標準のメソッドで、よく使われるものを教えてください。
.sqrt() は i32 にはありませんでしたが f64 にはありました。
i32 のメソッドをもっと教えてください。
続きを教えて。
i32 型の checked_add()
メソッドのシグネチャを教えてください。
fn main() {
let x : i32 = 10;
let y = x.checked_add(3);
let z = x.checked_add(i32::MAX);
}
で y と z を標準出力する方法を教えてください。
続きを教えて。
"{:?}" を使えば、Option<i32>
型の値を println! で表示できるみたいですね。
Rust の if は C++ と見た目が違いますね。
続きを教えて。
(コードと本文が反転しちゃった)
if で { } は省略できないのですね。
x が 0 以上 10 以下であるかを調べるコードはこれで良いですか?
fn main() {
let x = 10;
if 0 <= x && x <= 10 {
println!("yes");
}
}
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));
}
なるほど、ありがとうございます。タプルを気軽に使えるのは C++ より便利ですね。今日はここまでにします。明日はループについて学ぶ予定です。また明日よろしくお願いします。
5 日目はここまでです。
いつも通りセッションをリセットして、明日も前日の記憶が無い ChatGPT から Rust を教わります。
ここから 6 日目
私は ChatGPT と会話しながら Rust を学んでいて、今日はその 6 日目です。私は C++ 経験者ですが、Rust の機能や文法についての知識はありませんでした。1 日目は Rust の特徴や、Hello world プログラム、println! マクロ、変数の宣言、整数の標準出力について、2~4 日目は文字列やそのメソッドについて、5 日目は演算子と関数、if について学びました。今日は Rust におけるループを学びたいと思います。
ありがとうございます。
for i in 0..10 {
println!("{}", i);
}
は 0 から 9 を出力しましたが、9 から 0 を出力する場合はどうすれば良いですか?
.. について説明してください。上記以外に使い方の例がありますか?
続きを教えて。
なるほど、
fn main() {
let a = 1..10;
let b = 1..=10;
println!("{:?}", a);
println!("{:?}", b);
}
で a, b はそれぞれ何型ですか?
for, while, loop では break; を使えますか? サンプルコードを書いてください。
わかりました。C++ とだいたい同じですね。二重ループの内部から break する方法はありますか?
なるほど、便利ですね。ラベルは ' を付ければよいのですか。ラベルの名前のルールについて教えてください。
ループにおける break の使い方は
・break;
・break ラベル;
のほかにありますか?
ループにおける continue の使い方は
・continue ;
・continue ラベル;
の 2 つ以外にありますか?
わかりました。整数の配列があって、その各要素を出力するコードを教えてください。
そのサンプルで、ループを使って配列の要素をすべて 2 倍にする場合はどうしますか?
上記のコードで numbers の型は何ですか?
その配列は固定長の配列ですか?
固定長の配列のメソッドを教えてください。
続きを教えて。
わかりました。Vec の機能を学べるサンプルを作ってください。
メソッド一覧を教えて。
続きを教えてください。
続きを教えて。
Vec にある sort(), reverse() のようなメソッドをほかにも教えてください。
コードの続きを教えて。
コードの続きを教えて。
Vec は vec! マクロでも作れますよね?
1..=5 を Vec に変換するにはどうすれば良いですか?
Vec の要素を降順にソートするサンプルを教えてください。
sort_by() の引数は C++ でいうラムダ式のようです。Rust では何と言いますか?
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
の要素の合計、平均、最大値を求めるコードを書いてください。
最大値のところは * を付けて
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() の戻り値の型を教えてください。
指定した値と同じ要素の個数を調べる方法 (C++ でいう std::count()) を教えてください。
String で filter() や sort() を使う場合はどうすれば良いですか?
入力した文字列(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(正解)になった。実際の提出)
続きを説明して。
続きを説明して。
trim() を使うのもありですよね。今日はありがとうございました。Rust でいろいろなプログラムが書けるようになってきました。明日は match について学びたいと思います。
6 日目はここまでです。
いつも通りセッションをリセットして、明日も前日の記憶が無い ChatGPT から Rust を教わります。
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 の応答は、非公開のリポジトリにテキスト形式でもバックアップしています。研究目的等でテキストデータを必要とする方はお気軽にご相談ください。