呪われたRust:間違った方法で出力する
原文は Matthias Endler により に公開されました。 このブログを購読する

コペンハーゲン大学の試験にまつわる、有名な物理学者の話がある。受験者は、気圧計を使って超高層ビルの高さを測定する方法を説明するよう求められた。学生は、ビルの屋上から気圧計を紐で吊り下げ、紐の長さに気圧計自体の高さを加えて測ればよいと答えた。技術的には正しいものの、試験官たちは面白がらなかった。
異議申し立てと再評価ののち、学生は物理学に基づいたさまざまな解法を提示した。気圧計を落下させて落下時間からビルの高さを計算する方法から、ビルと気圧計の影の長さの比率を利用して気圧計の高さからビルの高さを算出する方法まで多岐にわたる。さらには、気圧計と引き換えに管理人に直接尋ねればよいというユーモラスな提案までした。
伝説によれば、この物理学者は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_fmtの呼び出しへと脱糖されるが、これはWriteトレイトのメソッドである。
これには実用的なユースケースもある。とにかく高速に出力したい場合、stdoutを一度だけロックしてからwrite!を使うことができる。これにより、println!を呼び出すたびにstdoutをロックするオーバーヘッドを回避できる。このテクニックを使った超高速なyesの書き方については、こちらの記事を参照してほしい。
解決策2:文字ごとにイテレートする
"Hello, world!".chars().for_each(|c| print!("{}", c));これは、Rustの強力なイテレータを使ってprintln!を実装できることを示している。ここでは文字列の各文字をイテレートし、1文字ずつ出力している。
chars()はUnicodeスカラー値に対するイテレータを返す。
イテレータについて詳しくはこちらを参照。
解決策3:Displayを実装する
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:I ♥︎️ クロージャ
(|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 Shl for cout {
type Output = cout;
fn shl(self, data: T) -> Self::Output {
print!("{}", data);
cout
}
}
impl Shl 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::>磷));
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コードコンテストも楽しめるかもしれない。過去の受賞作品はこちらからチェックしてほしい。
もし気圧計の話の方により興味を惹かれたなら、型破りな思考で知られるもう一人の有名な物理学者でありノーベル賞受賞者でもあるリチャード・ファインマンの著書『ご冗談でしょう、ファインマンさん』を読んでみてほしい。
私たちは皆、既成概念にとらわれず、問題に対して型破りな解決策を考え出すよう努めるべきだ。もしかしたら、それこそが問題そのものをより深く理解する鍵なのかもしれない。
記事をランダムに読む
コメント
ログインしてコメントする