GHC拡張ユーザーガイドでTypeApplications拡張の記述を読んだので、概要をまとめようと思います。この記事ではさわりだけまとめたものとなるので、TypeApplications拡張の詳細が気になる方はGHC拡張ユーザーガイドをご覧ください。
TypeApplications拡張とは
TypeApplicationsは「型変数を持つ関数に対し、その型変数に適用する型を明示的に指定することができる」ようになる拡張機能です。
これだけだとピンと来ませんので、例を見てみます。
※kindに対するapplicationも存在するようですが、ここら辺は触っていないので割愛。
使用例
以下のようなソースコードがあり、結果として、Int型の値「1」が出力されてほしいとします。
main :: IO ()
main = print $ show (read "1")
ところが、上記のコードは以下のようなコンパイルエラーが発生します。
• Ambiguous type variable ‘a0’ arising from a use of ‘show’
prevents the constraint ‘(Show a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
(中略)
• Ambiguous type variable ‘a0’ arising from a use of ‘read’
prevents the constraint ‘(Read a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
(以下、略)
これは、show関数およびread関数の型注釈で宣言されている型変数に対し、どの型を具体的に適用するか特定できないために起こります。(Show/Read型クラスのインスタンスは複数存在するため、どのインスタンスを適用するのか特定ができない)
read :: Read a => String -> a
show :: Show a => a -> String
TypeApplications拡張を有効にすると、read関数やshow関数の型注釈で宣言されている型変数に対して具体的な型を指定することができます。以下のコードはコンパイルが通り、「"1"」が結果として出力されます。
{-# LANGUAGE TypeApplications #-}
main :: IO ()
main = print $ show (read @Int "1")
TypeApplicationsの記法
(型変数に対して)具体的な型を適用する関数に続けて、「@(適用する型)」の形で書きます。
上記のコードを、もう一度見てみましょう。
{-# LANGUAGE TypeApplications #-}
{-
read関数の型変数aに対して、Intを適用することを明示
結果として、
read :: String -> Int
show :: Int -> String
と型解決ができるようになる。
show関数の型変数に適用する型を明示する場合は、show @Int (read "1")のように書く
-}
main :: IO ()
main = print $ show (read @Int "1")
read関数の型変数に適用する型をIntと明示したので、コンパイラが型解決できるようになりました。
型変数が複数宣言されている関数に適用する場合
型変数の順序に合わせて、適用する型を明示します。
-- map :: (a -> b) -> [a] -> [b]
map @Int @Int (\x -> x * 2) [1, 2] -- 型変数a, bにIntを指定
map @Int @_ (\x -> x * 2) [1, 2] -- 型変数aにIntを指定。"@_"は型の指定をしない時に使える
map @Int (\x -> x * 2) [1, 2] -- 型変数aだけにIntを指定
TypeApplicationsを適用するためのルール
TypeApplicationsを使用する際には、いくつかルールを守る必要があります。
@マークの前にはスペース文字を入れる
スペースがないと、以下のエラーが発生します。
※ただし、型名の後にスペースを入れる必要はないようで、read @Int"1"
と書いてもコンパイルできました。
Type application syntax requires a space before '@'
specifiedな型変数であること
以下、GHCユーザーガイドの記述によると、GHCは型変数に対してinferredとspecifiedのどちらかであるかを区別しており、型変数がspecifiedの時のみTypeApplicationsを適用できます。
(以下、GHCユーザーガイドより抜粋)
GHC tracks a distinction between what we call inferred and specified type variables. Only specified type variables are available for instantiation with visible type application.
一般的に型変数がspecifiedであるとは、「ソースコード上に型変数を書いてあること」だそうです。なので、型注釈が宣言されている関数の型変数はspecifiedとみなされます。
逆に関数に型注釈を書いていなければ、inferredな型変数とみなされ、TypeApplicationsの記法を適用することができません。これはGHCが型変数がどのような順序で宣言されているか確定できないことから、型安全性を守るために禁じています。
例えば、以下の関数fは型注釈がされているので、型変数に具体的な型を指定できますが、関数gは型注釈がないため、型指定ができません。
f :: (Num a) => a -> a -> a
f x y = x + y
g x y = x + y
Prelude> f @Int 1 2
3
Prelude> g @Int 1 2 --gは型注釈による型変数宣言がないため、TypeApplications拡張の記法を適用することができない
<interactive>:4:1: error:
• Cannot apply expression of type ‘a0 -> a0 -> a0’
to a visible type argument ‘Int’
• In the expression: g @Int 1 2
In an equation for ‘it’: it = g @Int 1 2
TypeApplications拡張のメリット
①型を明示しているため、コードがわかりやすくなる
②型注釈に比べて、少ないコード量で型指定ができる
以下は、JSONファイルを読み込んで、結果をコンソールに出力するプログラムですが、
TypeApplications拡張の便利さがわかると思います。
1 {-# LANGUAGE TypeApplications, TemplateHaskell #-}
2
3 import Data.Aeson
4 import Data.Aeson.TH
5 import qualified Data.ByteString.Lazy.Char8 as B
6 import Control.Monad
7
8 data Person = Person
9 { personName :: String
10 , personAge :: Int
11 } deriving (Show)
12
13 deriveJSON defaultOptions ''Person
14
15 main :: IO ()
16 main = do
17 -- (個人の好みもあると思いますが)型注釈を用いたコードの場合、若干記述のスマートさに欠ける感じがある
18 B.readFile "person.json" >>= \json -> print $ (decode json :: Maybe Person)
19 print . (decode :: B.ByteString -> Maybe Person) <=< B.readFile $ "person.json"
20 -- "decode :: FromJSON a => B.ByteString -> Maybe a" の型変数aに、Personを指定することで、コードが簡潔でわかりやすくなった
21 print . (decode @Person) <=< B.readFile $ "person.json"
上記のソースコードの18,19,21行目は全て同じ結果を出力しますが、TypeApplications拡張を使用したコードが一番読みやすくないでしょうか。
TypeApplicationsってとても便利ですね。