2008年11月10日月曜日

Haskell の返り値の型を指定する関数 - maxBound, minBound, (=~), return

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 関数を使って)

read :: Read a => String –> a

先ほどと同じように、返り値の型を指定しないと、

*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 関数のように、返り値に特定の型が指定されている関数もあれば、

length :: [a] -> Int

返り値として、型変数が使われている関数もある。

例えば

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

read :: Read a => String –> 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 で包むモナドがわかるということかな。