Rustで強めに型をつけるPart 5: 「なんでも文字列」をやめる
κeenです。「強めに型をつける」シリーズです。 いつか書こうと思って後回しにしてたら全然書かなかったので寝れない夜に筆をとります。 特にスクリプト言語を使ってた人にありがちな「なんでも文字列」について。
スクリプト言語だと文字列の扱いが簡単ですし、操作する関数も色々あるのでついつい何にでも文字列を使ってしまいがちです。
例えば状態をあらわすのに文字列を使ったりしてませんか?
// "朝", "昼", "夜"で時間帯を表わす
fn hello(period: &str) -> &str {
// 文字列にパターンマッチできる。便利!
match period {
"朝" => "おはようございます",
"昼" => "こんにちは",
"夜" => "こんばんは",
// Rustがエラーを出すのでそれ以外の場合も処理しておく
_ => unreachable!(),
}
}
あるいは、構造を表わすのにも文字列を使ったりしてませんか?
例えば座標 $(1,1)$ を表わすのに "1,1"
を使うだとか。
流石に座標でこれをやる人は少ないでしょうが(私はRubyでやったことがあります)、URLとかだとやったことがある人もいるんじゃないでしょうか:
# rubyだと正規表現が便利なのでこういう書き方をしたことがある人もいるかも
url = "https://example.com/foo/bar"
domain = url[/(https?):\/\/([a-zA-Z0-9.]+)/, 2]
こういうのは気軽に使えて便利な一方、特に静的型付き言語ではデメリットもあります。
- バリデーションをいつ行っているか分かりづらい
- 構造のパースが何度も走って無駄
- URLのように仕様が複雑なものだと間違ってパースする可能性が高い
- パース処理があちこちに散らばるのでフォーマットの変更に弱くなる
- メソッドを呼ぶときに何を期待しているのか、型を見ただけでは分かりづらい
- どのタイミングでも不正な入力を渡せてしまうので、常に例外のことを考えないといけない
加えて、Rustでは文字列処理はスクリプト言語ほど気軽ではないのでただただつらいだけになります。 こういうところでは用途ごとに型を定義してあげて、それを使うことで上記のデメリトを解消できます。
最初の hello
の例だとこうですね。
// 期待する値しかとらないデータ型を定義しておく
enum Period {
Morning,
Day,
Night
}
fn hello(period: Period) -> &'static str {
use Period::*;
match period {
Morning => "おはようございます",
Day => "こんにちは",
Night => "こんばんは",
// enumなので期待しない値のことは考慮しなくてよくなる
}
}
3種類のラベルしか受け取らないなら、3種類の値しかとらない enum
を定義してあげます。
簡単ですね。パターンマッチでも例外的な値のことを考えなくてすみます。
座標は "1,1"
ではなくて Point
型を作るほうがよさそうです。
// 文字列に構造があるなら、構造体を作ったほうがいい
struct Point(i32, i32);
Point(1, 1)
URLの例だと、外部ライブラリ、 urlなんかを使います。
// Rustだと外部ライブラリを使った方が便利
use url::Url;
let url = Url::parse("https://example.com/foo/bar").expect("invalid url format");
let domain = url.domain();
あとは外部とのやりとりのために文字列とデータ型を行き来するだけです。 外部とのやりとりのとこでだけフォーマットのパースや値のバリデーションを行い、残りのプログラムでは綺麗な世界だけでプログラミングができます。
私感ですがRustの文字列処理が難しいといっている人の何割かは不要に文字列を使ってるせいで難しくなってるんじゃないかと思っています。
もちろん、Rustでは文字列に String
と &str
があったり &str
のライフタイムを考えたりと扱いが難しいことは否定しません。
ただ私がRustを書いていて文字列処理をすることはそう多くないので、やたら文字列をこねくり回してるなーと思ったら一度本当に文字列が適切か見直してみてもいいんじゃないでしょうか。