Rustにはstd::str::FromStrというtraitがあり,データ型がこれを実装すると,from_strという名前のassociated function*1を通じて,strからそのデータ型に変換できるようになる.
use std::str::FromStr;
fn main() {
let x = i32::from_str("42");
println!("{}", x.unwrap()) // 42
}
これだけ見れば,特に取り立てて議論するべき点はない.
一方.strはparseというメソッドを持っていて,文字通り文字列のパーズを行うのだが,以下のようなシグネチャをしている.
fn parse<F>(&self) -> Result<F, F::Err>
where F: FromStr
strである自身を受け取って,Result<F, F::Err>型を返す—ただし,FはFromStr traitを実装している*2—といったところだ.前述したstd::str::FromStr::from_strと同じことをしているが,いわば見る視点が逆転しているのである.つまり,std::str::FromStr::from_strは,Selfからstrを,std::str::parseは,strからFを,それぞれ眺めている.
さて,前者はstrに視点を定めればよいのは明らかだが,立場が逆になるとうまくいかない.Fは多相なので,どこを見ればよいかが定かでない.
具体例を挙げよう.次のコードはコンパイルできない."42"という文字列をどの型の値としてパーズしたいかがわからないからだ.
fn main() {
let x = "42".parse();
println!("{}", x.unwrap())
}
/*
error[E0284]: type annotations required: cannot resolve `<_ as std::str::FromStr>::Err == _`
--> /var/folders/5h/7wt7yl7n24v72zsz77_3w_w00000gn/T/vKY0lB7/51.rs:2:18
|
2 | let x = "42".parse();
| ^^^^^
*/
エラーメッセージに従って,型注釈を与えてやれば,コンパイルに成功する.
use std::num::ParseIntError;
fn main() {
let x: Result<i32, ParseIntError> = "42".parse();
println!("{}", x.unwrap()) // 42
}
ところで,i32のパーズに失敗した際のエラーはParseIntErrorだと分かり切っている.i32という記述から推論させられないものだろうか.
実はRustは,多相であるparseメソッドに,変換先の型の情報を渡して,型を限定する文法を提供している.これは"turbofish"と呼ばれ::<>という形をしている.先程のコードをturbofishを使って書き直す場合,parseメソッドとメソッド呼び出しの()の間に::<i32>と書いてやる.
fn main() {
let x = "42".parse::<i32>();
println!("{}", x.unwrap()) // 42
}
関数の型(&str -> Result<i32, ParseIntError>)を直接指定しているのではなく,関数に型i32を,あたかも引数のように与えているという点に注目してほしい.これはで抽象化された型Fにi32という具体型を渡して,関数全体の型を決定するという操作に相当しているのだと思う.
さて,Haskellにこのような文法はなかったかと考えたが,先日Haskell Day 2016に赴いた際に,SPJがSystem Fの話をしていて,GHC 8.0から,Type Applicationという機能が導入された*3と話していたことを思い出した.これを用いると,Haskellでも以下のように@Intという記法で,多相な関数にInt型を適用して単相化することができる.
{-# LANGUAGE TypeApplications #-}
import Text.Read (readEither)
unwrap :: Either a b -> b
unwrap = either undefined id
main = print $ unwrap (readEither @Int "42") -- 42
RustのResultに対応して,Eitherを用いた.
GHCiを使えば,多相な関数に型を適用して,単相な関数にする過程を実際に確かめることができる.
$ ghci
> :set -XTypeApplications
> :t read
read :: Read a => String -> a
Prelude> :t read "42"
read "42" :: Read a => a
> read "42"
*** Exception: Prelude.read: no parse
> :t read @Int
read @Int :: String -> Int
> :t read @Int "42"
read @Int "42" :: Int
> read @Int "42"
42
Hindley-Milner型システムは強力であり,プログラマが直接型を明示しなければコンパイルできない,といった状況はそう多くない.しかしながら,幾つか例外があり,そのような場合に値ないし関数に,完全な形で注釈を与えることは,しばしば煩わしい作業である.let x: Result<i32, ParseIntError>や(read :: String -> Int) 42などという冗長な書き方をしなくても済むように,このような仕組みを言語や処理系が提供してくれることは心強い.
以上の内容は,rustc 1.12.0およびghc 8.0.1での挙動に基づいている.
脚注#
*1: 引数としてselfを取らないもの.他の言語で言うstatic methodやclass methodなどに相当する.
*2: 個人的にこのwhereというキーワードは(少なくとも初学者にとっては)Haskellにおける=>よりもわかりやすいと思っている.Rustの影響を色濃く受けるSwiftでも採用されている.
*3: 実際には,以前から内部的に実装されていたものを,一部使えるようにしたらしい.