Help us understand the problem. What is going on with this article?

GHC拡張のTypeApplicationsについて

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 :: Read a => String -> a
show関数の型注釈
show :: Show a => a -> String

TypeApplications拡張を有効にすると、read関数やshow関数の型注釈で宣言されている型変数に対して具体的な型を指定することができます。以下のコードはコンパイルが通り、「"1"」が結果として出力されます。

TypeApplications拡張を有効にしたソースコード
{-# LANGUAGE TypeApplications #-}                                                                                                                                                                                                         

main :: IO ()
main = print $ show (read @Int "1")

TypeApplicationsの記法

(型変数に対して)具体的な型を適用する関数に続けて、「@(適用する型)」の形で書きます。
上記のコードを、もう一度見てみましょう。

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ってとても便利ですね。

masaki_shoji
ソフトウェアエンジニア。株式会社G・B・S所属。 仕事では主にAndroid開発を担当する事が多いですが、システム全般に興味があります。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした