ChatGPTにツッコミを入れる
Rust を ChatGPT だけで勉強する【実験記録】 のChatGPTの回答の不正確な部分を指摘してみます。
ChatGPT の回答の正確性や課題についての考察は本記事では述べず、Rust の専門家に任せたいと思います。
とのことなので、まあ私がRustの専門家かと言うと、もっとできる人はいるだろうと思いますが、マサカリを投げるのは好きなのでChatGPTにならいくらでもマサカリを投げて良いだろうということで。重箱の隅をつつくような指摘を入れていきたいと思います。
1日目
いきなり細かい話ですが、ちょっと引っかかってしまったのがこの部分です。まあこれはChatGPTがどうこうというより、世間一般でそういう捉えられ方をされていると思いますが、実際に使っている身からすると、RustはあまりC++と似ているとは言えません。
例えば関数一つ書くにも、
int f(int x) { return 2 * x; }
とするC++と、
fn f(x: i32) -> i32 { 2 * x }
とするRustは果たして似ているでしょうか?
確かに、{}
でブロック構造を表したり()
で関数呼び出しを行ったりするあたりはC++と同じようにC言語からの流れを汲んでいますし、ジェネリクスに<>
を使用するあたりはC++の影響を受けてはいます。ですが、Rustは色々な先発言語の特徴を受け継いでおり、C++にそこまで寄せている訳ではありません。たとえば、クロージャを||
で表すのはRubyのブロックの影響を受けています。Rustのカバーする分野はC++と競合するので、精神的な後継と言えなくもないですが、C++に似せて作られてはいないと思います。
ChatGPTは「より高速な処理を実現するため」と言っていますが、println!
がマクロになっている理由はそうではありません。
もちろん、println!
マクロはオーバーヘッドが少なくなるように実装されていますが、println!
がマクロなのは、マクロでしか実装できないからです。
println!
マクロは第一引数に文字列リテラルのみを受け取ります。フォーマット文字列に&str
型の変数などを渡すことはできません。渡された文字列リテラルはコンパイル時にパースされ、プレースホルダーで分割され、プレースホルダーの数や型が合っているかチェックされます。これらは関数では実現できません。
また、RustにはC言語との互換性のためのものを除いて、可変長引数がサポートされていません。つまり任意個の引数を取るものはprintln!
と同様マクロで表現する必要があります。vec!
がマクロで用意されているのも同様の理由によるものです。
そんなものはない。
println!
の実際の定義を確認してみましょう。こうなっていますね。
macro_rules! println {
() => {
$crate::print!("\n")
};
($($arg:tt)*) => {{
$crate::io::_print($crate::format_args_nl!($($arg)*));
}};
}
という訳で実際には ::std::format_args_nl!
マクロでフォーマット文字列と引数からcore::fmt::Arguments
という構造体を構築して std::io::_print
関数に渡すということをやっています。std::format_args_nl!
マクロとか追いかけてみるとコンパイラマジックにぶち当たってしまうのですが、実際にどうマクロ展開されるかを知りたい方は cargo-expandとか使ってみると良いでしょう。
とりあえずChatGPTむっちゃ大嘘つくじゃん、ということが分かっていただけるかと思います。
そんなことはない。
Rustが(no_stdでない場合に)自動的にインポートするものは、std::prelude
モジュールにおいて再エクスポートされたものになります。他のモジュール単位で自動的にインポートされるものはありませんし、Prelude contentsの部分にstd::io
モジュールの中身が入ってすらいません。
Rustではuse
で明示的なインポートをせずとも、その識別子のフルネームを記述すれば利用することができます。std::io
モジュールを自動的にインポートせずとも、::std::io::_print
と書けば_print
関数を使うことは可能ということです。
ここまで読んだみなさんはもうChatGPTが滅茶苦茶嘘をついているということが分かっていただけると思います。
自明に嘘、と言えるほどの表現ではないですが、怪しい言い方です。
Rustのマクロは確かにC++のマクロよりできることが多いですが、本質的にはソースコードレベルでの文字列の置き換え、つまりマクロが書かれた部分がコンパイル時のマクロ展開フェーズでマクロを含まないソースコードになるという仕組みです。つまり、マクロの展開後のソースコードが文法的あるいは意味論的に間違っていればエラーになります。マクロ展開前はエラーにならなかったコードが展開後にエラーになることはなく、手動で置き換えた時にそうなるなら展開を間違えているということです。ここらへん、直前で変なことを言ってしまったために後に引けなくなっている様子がうかがえます。
そんな機能はない。
上述のように、マクロは文字列の置き換えを行っているため、展開後のコードでモジュール内部に隠されたもの(つまりpub
ではない関数など)を呼び出すことはできません。std::io::_print
はドキュメントには存在しませんが(#[doc(hidden)]
がついているため)、外部から普通に見える関数です。ChatGPTは間違えてstd::io::println
という存在しない関数を出してしまったがために、存在しない機能をでっち上げる必要に迫られてしまったようです。
慣習ではなく、文法的にマクロ呼び出しは!
が必要です。
みなさんもうご存じ、大嘘です。
嘘です。これまでに確認してきたように、println!("x = {}", x);
は
::std::io::_print(::std::format_args_nl!("x = {}", x));
に展開されます。
嘘です。format_args_nl!
の実装はコンパイラマジックですが、cargo-expandのREADMEを読む限りでは、format_args_nl!("x = {}", x);
を展開すると、
::core::fmt::Arguments::new_v1(
&["x = ", "\n"],
&match (&x,) {
(arg0,) => [::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt)],
},
)
のようになると思われます。ChatGPTは実際には使われていないformat_args!
を挙げていますが、format_args!
はformat_args_nl!
の末尾に\n
が付かない版なのでまあ似たような感じに展開されるはずです。ここらへんはコンパイラ内部に実装されているので必ず同じように展開されるとは限りません。
main
関数は戻り値を返すことができますが、これは嘘です。main
関数の戻り値にはstd::process::Termination
トレイトが実装された型を指定しないといけません。単純に終了コードを返したい場合、 i32
ではなく std::process::ExitCode
を返すことができます。
できません。上述のようにそもそもmain
の戻り値をi32
にすることはできないのですが、仮にそうできたとしても、戻り値がi32
の関数ではreturn
を書くか、あるいは関数ブロック末尾にi32
型の式を持ってくる必要があります。
Rustで戻り値を省略できるのは戻り値型が()
の時だけです。()
にはTermination
が実装されているのでmain
関数の戻り値を()
にすることはできますし、その場合はreturn
を書く必要はありません。もっとも、これは()
を返す関数一般に言えることでmain
に限った話ではありません。
これは細かいことなのでRustのコメントはC++とほぼ同じと言ってしまってもいいのですが、Rustの/**/
はネストさせることができます。
/* コメントここから
/* これはコメントの開始ではない
コメントここまで */
ここはコメントではない */
/* コメントここから
/* ネストされたコメント開始
ネストされたコメントここまで */
コメントここまで */
という訳で、厳密に同じとは言えません。
1日目のツッコミどころはだいたいこんなところです。