1. 返り値が多相な関数は、返り値の型を指定する
Haskell には、返り値が多相な関数がある。
例えば、Prelude に定義されている Bounded のクラスメソッド。
class Bounded a where minBound, maxBound :: a
「型変数 a は、Bounded クラスのインスタンスである」という制約が、表現されている。
(cf. Haskell の数値 – Int は型で Num は型クラス)
GHCi で確認する。返り値の型を指定しないで、関数を呼び出すと、
*Main> maxBound:1:0: Ambiguous type variable `a' in the constraint: `Bounded a' arising from a use of `maxBound' at :1:0-7 Probable fix: add a type signature that fixes these type variable(s)
「曖昧な型だ」と警告がでる。
Bounded クラスのインスタンスを、maxBound の返り値の型として指定すると、
*Main> maxBound :: Int 2147483647 *Main> maxBound :: Bool True *Main> maxBound :: Char '\1114111' *Main> maxBound :: Ordering GT
それぞれの型の maxBound の値を返してくれる。
2. 文脈から型推論できると、型の指定は省略できる
read 関数も同じく、返り値の型が多相となっている。
(cf. Haskell で、繰り返しのある数値を生成する関数を定義 – read, cycle 関数を使って)
先ほどと同じように、返り値の型を指定しないと、
*Main> read "100":1:0: Ambiguous type variable `a' in the constraint: `Read a' arising from a use of `read' at :1:0-9 Probable fix: add a type signature that fixes these type variable(s)
「曖昧な型だ」と警告が出る。
返り値の型として、Read クラスのインスタンスを指定すると、値を返してくれる。
*Main> read "100" :: Int 100 *Main> (read :: String -> Int) "100" 100 *Main> read "True" :: Bool True *Main> read "EQ" :: Ordering EQ
重要なのは、次のように、適用するときの文脈があると、型を指定しなくてもいい。コンパイラが型を推論してくれる。
*Main> read "100" + 1 101 *Main> read "True" && True True *Main> read "True" && False False
3. バラエティに富んだ返り値の型の例
正規表現の (=~) も同様に、返り値の型を指定する必要がある。
(cf. Haskell で正規表現 (2) =~)
関数の型を見ると、以下のように表現されている。
(=~) :: (RegexMaker Regex CompOption ExecOption source, RegexContext Regex source1 target) => source1 -> source –> target
相変わらず、これどうやって読むかわからない。 ^^; それは横に置いておき、この関数も、返り値の型を指定しないとエラーが表示される。
*Main>:m Text.Regex.Posix Prelude Text.Regex.Posix> "hoge" =~ "ho":1:0: No instance for (Text.Regex.Base.RegexLike.RegexContext Regex [Char] target) arising from a use of `=~' at :1:0-13 Possible fix: add an instance declaration for (Text.Regex.Base.RegexLike.RegexContext Regex [Char] target) In the expression: "hoge" =~ "ho" In the definition of `it': it = "hoge" =~ "ho"
返り値の型を指定すると、型に応じた結果を返してくれる。
Prelude Text.Regex.Posix> "hoge" =~ "ho" :: Bool True Prelude Text.Regex.Posix> filter (=~ "ho") ["hoge","piyo","fuga","hogehoge"] ["hoge","hogehoge"] Prelude Text.Regex.Posix> "hoge" =~ "ho" :: (String,String,String,[String]) ("","ho","ge",[]) Prelude Text.Regex.Base> :m Text.Regex.Base Text.Regex.Posix Prelude Text.Regex.Base Text.Regex.Posix> "hoge" =~ "ho" :: (MatchOffset,MatchLength) (0,2)
同じ値に、同じ演算子(関数)を適用しているにも関わらず、型の指定を変えると、異なる値が返ってくる。
Java との比較
上記の (=~) 演算子の動作を初めて見たとき、
「 なぜ同じ関数名で、しかも、引数も同じなのに、異なる型の値が得られるのだろう」
と思った。
例えば Java の場合、「Java言語規定 クラス」を見ると、
メソッドの シグネチャ(signature) は,メソッドの名前並びにメソッドに対する形式仮引数の個数及び型で構成する。クラスは,同じシグネチャをもつ二つのメソッドを宣言してはならない。
つまり、以下のように、「引数が同じで、返り値の型が異なる hoge メソッド」を 2 以上定義することはできない。
public class Hoge { void hoge(){} int hoge(){ return 1;} String hoge() { return "hoge";} }
だから、上記の Haskell の関数の使い方を見て
「なんでそんなことができるのだろう?」
と感じた。
4. 返り値は型変数だけれど、引数を持つ関数との比較
Prelude を見ると、length 関数のように、返り値に特定の型が指定されている関数もあれば、
返り値として、型変数が使われている関数もある。
例えば
map :: (a -> b) -> [a] -> [b]
sequence :: Monad m => [m a] -> m [a]
上記の maxBoound, read, (=~) 関数のように、返り値の型を指定してみる。
Prelude> let map' = \f xs -> map f xs :: [String] Prelude> :t map' map' :: (a -> String) -> [a] -> [String] Prelude> let sequence' = \xs -> sequence xs :: Maybe [Int] Prelude> :t sequence' sequence' :: [Maybe Int] -> Maybe [Int]
型の指定はできるが、このように定義した map’, sequence’ の使い途って、何かあるのかな?
引数による型の絞り込み
返り値の型を指定しなくてはいけない、上記 3 つの関数を並べてみる。
maxBound :: a
(=~) :: (RegexMaker Regex CompOption ExecOption source, RegexContext Regex source1 target) => source1 -> source –> target
これらの関数と比較すると、map, sequence などの関数は、引数を指定すると返り値の型が絞り込まれることがわかる。
例えば、map 関数の第1引数に関数を指定すると、
Prelude> :t map (++ "hoge") map (++ "hoge") :: [[Char]] -> [[Char]] Prelude> :t map (* 2) map (* 2) :: (Num a) => [a] -> [a]
sequence 関数の第1引数にリストを指定すると、
Prelude> :t sequence [Just 1, Just 2] sequence [Just 1, Just 2] :: (Num t) => Maybe [t] Prelude> :t sequence [[1,2,3],[10,20,30]] sequence [[1,2,3],[10,20,30]] :: (Num t) => [[t]]
これに対して、
「返り値の型を指定する必要のある関数」
は、引数からその返り値の型を推測できない。よって、型を直接指定するか、もしくは型を推測できるための文脈が必要になる。
5. 返り値の型を指定する必要のある関数を定義してみる
「返り値の型を指定する必要のある関数」に慣れるため、適当な例を考えてみる。
例えば、次のような状況。
ペットショップに「犬」や「猫」がいる。飼われる前は「名前」がない。主人が名前を付けることによってペットとなる。
型の定義
まずは、「犬」「猫」の型を作成。
data Dog = Dog | PetDog String deriving Show
data Cat = Cat | PetCat String deriving Show
- 接頭辞 `Pet’ が付いているデータコンストラクタを使って、作成される値がペットであることを表わし、フィールドに名前を持つ。
- `Pet’ が接頭辞が付いてない方を「飼われていない犬・猫」を表すとする。
返り値の型を指定する必要がある関数
「犬」「猫」共に、「名前を付けることができる」とする。両方に共通の Animal クラスを作成し、動物に名前を付ける name 関数を作成した。
class Animal a where name :: String -> a
この name 関数が、返り値の型を指定する必要がある関数。見てわかるように引数は String だけであり、これだけでは返り値である Animal クラスのインスタンスを推測することができない。
次に、Dog, Cat 型を Animal クラスのインスタンスとし、クラスメソッド name を実装する。
instance Animal Dog where name cs = PetDog cs
instance Animal Cat where name cs = PetCat cs
「名前」を引数として与えると、データコンストラクタ PetDog, PetCat を使って値を生成する。
試してみる
早速、name 関数を使ってみる。
main = do print $ (name "Tama" :: Cat) -- PetCat "Tama" print $ (name :: String -> Cat) "Tama" -- PetCat "Tama" print $ (name "Pochi" :: Dog) -- PetDog "Pochi"
関数に制約を付ける
次に、Animal クラスのメソッドではない、同様の機能を持った makePet 関数を作りたいとする。この場合、関数に制約を付ける。
makePet :: Animal a => String -> a makePet cs = name cs
これも引数が String だけなので、引数からだけでは返り値の型を推測することができない。
print $ (makePet "Pochi" :: Dog) -- PetDog "Pochi"
上記と同様な関数で、例えば引数に Animal クラスのインスタンスの値を与える関数を作成するなら、
makePet2 :: Animal a => a -> String -> a makePet2 _ cs = name cs
与えた引数によって返り値の型が決まる。
print $ makePet2 Cat "Tama" -- PetCat "Tama"
全体のソースコード
class Animal a where name :: String -> a data Dog = Dog | PetDog String deriving Show instance Animal Dog where name cs = PetDog cs data Cat = Cat | PetCat String deriving Show instance Animal Cat where name cs = PetCat cs makePet :: Animal a => String -> a makePet cs = name cs makePet2 :: Animal a => a -> String -> a makePet2 _ cs = name cs main = do print $ (name "Tama" :: Cat) print $ (name :: String -> Cat) "Tama" print $ (name "Pochi" :: Dog) print $ (makePet "Pochi" :: Dog) print $ makePet2 Cat "Tama"
6. Monad クラスの return メソッド
ここまで書いて、やっと気がついたことがある。
Monad の説明を読んでいて、最初に違和感を感じた return 関数。この関数が使われているのを見る度に、
何か重要なことを自分が理解していない
と感じていた。しかし、それが何なのか自覚できず。 (+_+)
return の型宣言を見ると、
return :: a -> m a
この関数も、引数を与えただけでは、
返り値は、Monad クラスのインスタンスで包む
というところまでしか決まらない。
例えば、
*Main> :t return 1 return 1 :: (Monad m, Num t) => m t *Main> :t return "hoge" return "hoge" :: (Monad m) => m [Char]
具体的にどのモナドで包むのか決めるには、型を指定する必要がある。
*Main> return 1 :: Maybe Int Just 1 *Main> return "hoge" :: Maybe String Just "hoge" *Main> return 1 :: [Int] [1] *Main> return "hoge" :: [String] ["hoge"]
型推論される例
Monad クラスのメソッド (>>=) は
(>>=) :: forall a b. m a -> (a -> m b) -> m b
第 2 引数で具体的な型を指定すれば、第1引数で包むモナドも決まる。
*Main> return 1 >>= Just Just 1 *Main> return "hoge" >>= Just Just "hoge" *Main> return 1 >>= print 1 *Main> return "hoge" >>= putStrLn hoge
関数を定義したときも同様に、
test :: a -> Maybe a test = return test2 :: a -> [a] test2 = return main = do print $ test 100 -- Just 100 print $ test "hoge" -- Just "hoge" print $ test2 100 -- [100] print $ test2 "hoge" -- ["hoge"]
return をはじめ見たとき、何でこれで上手く動作するのかわからなかったけれど、型が指定されていたのが理由だったということ。
前回見た sequence 関数だと、
sequence :: Monad m => [m a] -> m [a] sequence = foldr mcons (return []) where mcons p q = p >>= \x -> q >>= \y -> return (x:y)
これを値に適用した時の型を見れば、
*Main> :t sequence [Just 1, Just 2, Just 3] sequence [Just 1, Just 2, Just 3] :: (Num t) => Maybe [t]
こちらは、引数で与えたモナドによって返り値の型が決まり、 return で包むモナドがわかるということかな。
0コメント:
コメントを投稿