読者です 読者をやめる 読者になる 読者になる

Rust の turbofish と GHC 8 の Type Application ― または我々は如何にして多相な関数を単相化するか

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
}

これだけ見れば,特に取り立てて議論するべき点はない.

一方.strparse というメソッドを持っていて,文字通り文字列のパーズを行うのだが,以下のようなシグネチャをしている.

fn parse<F>(&self) -> Result<F, F::Err> 
where F: FromStr

str である自身を受け取って,Result<F, F::Err> 型を返す.ただし,FFromStr trait を実装している*2,といったところだ.前述した std::str::FromStr::from_str と同じことをしているが,いわば見る視点が逆転しているのである.つまり,std::str::FromStr::from_str は,Self から str を,std::str::parse は,str から F を,それぞれ眺めている.

さて,前者は str に視点を定めればよいのは明らかだが,F は多相なので,立場が逆になるとうまくいかない.どこを見ればよいかわからないからだ.具体例を挙げると,当然ながら次のコードはコンパイルできない.

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
}

これだけ見ると,単に煩わしさしか感じないのだが,Rust がおもしろいのは,多相である parse メソッドに,変換先の型の情報を渡して,型を限定する文法を提供している部分にある.これは turbofish と呼ばれ ::<> という形をしている.

fn main() {
    let x = "42".parse::<i32>();
    println!("{}", x.unwrap()) // 42
}

関数の型を直接指定しているのではなく,関数に型を,あたかも引数のように与えているという点に注目してほしい.これは Λ で抽象化された型 Fi32 という具体型を渡して,関数全体の型を決定するという操作に相当しているのだと思う*3

さて,Haskell にこのような文法はなかったかと考えたが,先日 Haskell Day 2016 に赴いた際に,SPJ が System F の話をしていて,GHC 8.0 から,Type Application という機能が導入された*4と話していたことを思い出した.これを用いると,Haskell でも以下のような書き方ができる.

{-# 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
GHCi, version 8.0.1: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /Users/Ryota/.ghci
Prelude> :set -XTypeApplications
Prelude> :t read
read :: Read a => String -> a
Prelude> :t read "42"
read "42" :: Read a => a
Prelude> read "42"
*** Exception: Prelude.read: no parse
Prelude> :t read @Int
read @Int :: String -> Int
Prelude> :t read @Int "42"
read @Int "42" :: Int
Prelude> 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:この辺は全然詳しくないので,間違ってたらあとでこっそり教えてほしい.

*4:実際には,以前から内部的に実装されていたものを,一部使えるようにしたらしい.