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

この記事は以下のページに移転しました.

blog.ryota-ka.me

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 は多相なので,立場が逆になるとうまくいかない.どこを見ればよいかわからないからだ.具体例を挙げると,当然ながら次のコードはコンパイルできない."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 だと分かりきっているので,これだけ見ると単に煩わしさしか感じないのだが,Rust がおもしろいのは,多相である parse メソッドに,変換先の型の情報を渡して,型を限定する文法を提供している部分にある.これは turbofish と呼ばれ ::<> という形をしている.先程のコードを turbofish を使って書き直す場合,parse メソッドとメソッド呼び出しの () の間に ::<i32> と書いてやる.

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

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

さて,Haskell にこのような文法はなかったかと考えたが,先日 Haskell Day 2016 に赴いた際に,SPJ が System F の話をしていて,GHC 8.0 から,Type Application という機能が導入された*4と話していたことを思い出した.これを用いると,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
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:実際には,以前から内部的に実装されていたものを,一部使えるようにしたらしい.