呪われたRust:ものを間違ったやり方で出力する

コペンハーゲン大学の試験で物理学者にまつわる有名な話があります。受験者は、気圧計を使って超高層ビルの高さを求める方法を説明するよう問われました。学生は、ひもで気圧計を建物の屋上から吊り下げ、ひもの長さに気圧計の高さを足せばよいと答えました。技術的には正しいものの、試験官たちは面白くありませんでした。
抗議を受けて再評価が行われると、その学生は物理学に基づくさまざまな解法を示しました。気圧計を落とし、落下時間から建物の高さを計算する方法から、建物の影の長さと気圧計の影の長さの比を使い、気圧計の高さから建物の高さを計算する方法まであります。さらには、気圧計と引き換えに管理人へ単に尋ねればよい、とユーモラスに提案さえしました。
伝説によれば、この物理学者はニールス・ボーアで、1922年にノーベル賞を受賞しました。この話は気圧計の問題としても知られています。
この話が面白い理由
この問いと考えられる答えには、重要な教育的な副作用があります。型破りな方法でも解にたどり着けること、そして多くの場合、そうした方法は正攻法よりも面白いことを学ぶ人に伝えてくれます。なぜなら、問題そのものについて何かを明らかにしてくれるからです。
定番の問いに対する型破りな答えから学ぶことには価値があります。ある程度、新しい考え方や問題解決の方法を育み、それはイノベーションに不可欠な要素です。
同じ原則をRustの学習に当てはめる
Rustを学ぶ本の最初の例の一つは、「Hello, world!」プログラムです。
fn main() {
println!("Hello, world!");
}Rustのインストールが正しく動作しているかを確かめる手軽な方法です。
しかし、少し遊んでこの課題を逆さまにすることもできます。println!を使わずに「Hello, world!」を出力する方法を探してみましょう。
できるだけ多くの型破りな解法を考えてみます。奇妙であればあるほどよいです! 以下の各解法を見ながら、なぜ動くのか、そこから何を学べるのかを理解してみてください。
これはミームとして始まったのですが、投稿が大きな注目を集めたので、記事としてまとめることにしました。
言うまでもなく、これらの解法はどれもプロダクションコードで使うべきではありません。代わりに、このエンタープライズ対応版のhello worldをご覧ください。
解法1:println!の糖衣構文を外す
use std::io::Write;
write!(std::io::stdout().lock(), "Hello, world!");この解法が面白いのは、println!が単なるマクロであり、文字列の末尾に改行文字を追加したwrite!の呼び出しへ展開されることが分かる点です。
実際のコードはもっと奇妙です。驚きたいなら、このファイルでprintを検索してください。write!自体も、Writeトレイトのメソッドであるwrite_fmtの呼び出しへ糖衣構文を外せます。
これには実用的なユースケースがあります。非常に高速に出力したいなら、stdoutを一度ロックしてからwrite!を使えます。こうすると、println!を呼ぶたびにstdoutをロックするオーバーヘッドを避けられます。このテクニックで非常に高速なyesを実装する方法は、こちらの記事をご覧ください。
解法2:文字を反復処理する
"Hello, world!".chars().for_each(|c| print!("{}", c));これは、Rustの強力なイテレータを使ってprintln!を実装できることを示しています。ここでは文字列の文字を反復処理し、一文字ずつ出力します。
chars()はUnicodeスカラー値に対するイテレータを返します。
イテレータについてはこちらで詳しく学べます。
解法3:Displayをimplする
struct HelloWorld;
impl std::fmt::Display for HelloWorld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Hello, world!")
}
}
println!("{HelloWorld}");これにより、Rustでトレイトがどう動くのかを少し学べます。Displayトレイトを実装する構造体を定義しており、それによってprint!で出力できるようになります。一般にDisplayはより複雑な型を出力可能にするためのものですが、ハードコードした文字列に対して実装することもできます!
解法4:Displayなんて必要?
Displayを使う代わりに、自分たちのトレイトを作ってはどうでしょう?
trait Println {
fn println(&self);
}
impl Println for &str {
fn println(&self) {
print!("{}", self);
}
}
"Hello, world!".println();トレイトのメソッドには好きな名前を付けられるという事実を利用できます。この例ではprintlnという名前を選び、標準ライブラリの一部であるかのように見せています。
これはprintln!マクロを完全に逆転させています。文字列を引数として渡すのではなく、文字列そのものに対してメソッドを呼び出しています!
解法5:panic!があるのにprintln!なんて必要?
panic!("Hello, world!");Rustで出力する方法はprintln!だけではありません。この場合はpanic!を使います。これは副作用として文字列を出力し、即座にプログラムを終了します。出力したい文字列が一つだけなら動きます……。
解法6:クロージャが大好き ♥︎️
(|s: &str| print!("{}", s))("hello");Rustでは、クロージャを定義した直後に直接呼び出せます。このクロージャは、文字列スライスを引数に取り、それを出力する無名関数として定義されています。文字列スライスはクロージャへの引数として渡されます。
実際には、一度しか使わず、名前を考えたくもないクロージャを定義するときに役立つことがあります。
解法7:Cスタイル
extern crate libc;
use libc::{c_char, c_int};
use core::ffi::CStr;
extern "C" {
fn printf(fmt: *const c_char, ...) -> c_int;
}
fn main() {
const HI: &CStr = match CStr::from_bytes_until_nul(b"hello\n\0") {
Ok(x) => x,
Err(_) => panic!(),
};
unsafe {
printf(HI.as_ptr());
}
}出力するのにRustの標準ライブラリすら使う必要はありません! この例は、RustからC標準ライブラリのprintf関数を呼び出す方法を示しています。文字列を関数に渡すために生ポインタを使っているので、unsafeです。これにより、RustでFFIがどう動くのかを少し学べます。
この解法の功績はRedditの/u/pinespearと@[email protected]にあります。
解法8:C++スタイル
もう十分にサイコパスの領域へ入っています……ならば、ここで止まる理由はありません。とてつもなく頑張れば、Rustを意のままに曲げてC++のように見せられます。
use std::fmt::Display;
use std::ops::Shl;
#[allow(non_camel_case_types)]
struct cout;
#[allow(non_camel_case_types)]
struct endl;
impl<T: Display> Shl<T> for cout {
type Output = cout;
fn shl(self, data: T) -> Self::Output {
print!("{}", data);
cout
}
}
impl Shl<endl> for cout {
type Output = ();
fn shl(self, _: endl) -> Self::Output {
println!();
}
}
cout << "Hello World" << endl;Shlトレイトは<<演算子を実装するために使われます。cout構造体は、Displayを実装する任意の型に対してShlを実装しており、出力可能な型なら何でも出力できます。endl構造体はcoutに対してShlを実装しており、最後に改行文字を出力します。
この解法の功績はWisha Wanichwecharungruangにあります。
解法9:アセンブリによる混じり気なしの制御
こうした高水準の抽象化はすべて、効率よく出力する妨げになります。CPUの制御を取り戻さなければなりません。道はアセンブリです。無駄なサイクルはもうありません。隠れた命令もありません。純粋で、混じり気のないパフォーマンスです。
use std::arch::asm;
const SYS_WRITE: usize = 1;
const STDOUT: usize = 1;
fn main() {
#[cfg(not(target_arch = "x86_64"))]
panic!("This only works on x86_64 machines!");
let phrase = "Hello, world!";
let bytes_written: usize;
unsafe {
asm! {
"syscall",
inout("rax") SYS_WRITE => bytes_written,
inout("rdi") STDOUT => _,
in("rsi") phrase.as_ptr(),
in("rdx") phrase.len(),
// syscall clobbers these
out("rcx") _,
out("r11") _,
}
}
assert_eq!(bytes_written, phrase.len());
}やっていることがアセンブリコードの呼び出しだけなら、そもそもなぜRustを使うのかと思っているなら、要点を見失っています! これは単にものを出力する話ではありません。自由の話です! CPUをどう使うべきか、私に指図しないでください。
まあああ、x86_64マシンでしか動きませんが、自由のためなら小さな犠牲です。
isaacthefallenappleからの投稿です。
解法10:「爆速」
実際に使わないなら、あれだけのCPUコアにプレミアムを払ったのはなぜでしょう? 恐れなき並行性はRustの約束の一つではなかったのでしょうか? そのコアを有効活用しましょう!
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
let phrase = "hello world";
let phrase = Arc::new(Mutex::new(phrase.chars().collect::<Vec<_>>()));
let mut handles = vec![];
for i in 0..phrase.lock().unwrap().len() {
let phrase = Arc::clone(&phrase);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(((i + 1) * 100) as u64));
print!("{}", phrase.lock().unwrap()[i]);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!();
}ここでは、各文字を別々のスレッドで出力します。スレッドはループ内で生成され、各スレッドは一定のミリ秒だけスリープしてから担当する文字を出力します。文字列を出力するためにCPUの全パワーを使うのです! 常に正しい順序で文字が出力されるとは限りませんが(ほら、スケジューリングは難しい!)、生のパフォーマンス向上をすべて得られるなら、十分に価値のあるトレードオフです。
あなたの番です!
ほかにも解法があれば、ぜひメッセージを送ってください。
また、この記事を気に入ったなら、毎年開催される難読化Cコードコンテストも楽しめるかもしれません。過去の受賞作はこちらです。
気圧計の話のほうが実際に気になったなら、型破りな思考法で知られたもう一人の著名な物理学者・ノーベル賞受賞者、リチャード・ファインマンの著書『ご冗談でしょう、ファインマンさん』を読んでみてください。
私たちは皆、既成概念にとらわれず、問題に対する型破りな解法を考え出すよう努めるべきです。もしかすると、それこそが問題そのものをより深く理解する鍵なのかもしれません。
記事をランダムに読む