人気ブログランキング | 話題のタグを見る

Yet Another Haskell Tutorial 4

4.4 Function Types

この節では関数の型について説明してある。Haskell では、関数も first order values なので、数の 1 や、文字の 'c' などのように型を持っている。関数の型について述べるためには、関数のラムダ計算について知っておく必要がある。

管理人注:
first order values とか lambda calculus (ラムダ計算) とか理論的な話が出てくると面倒だが、要するに、関数が first order value であるということは、関数を関数の引き数にできることだと割り切るとよい。たとえば map という関数は (\x -> x * x) という関数を引き数にとって、リストの値を全て2乗することができる。

Prelude> map (\x -> x * x) [1,2,3,4,5]
[1,4,9,16,25]

関数 (\x -> x * x) は first order value だから関数 map の引き数にすることができる。

上の無名関数を記述する (\x -> x * x) が関数のラムダ式だ。引き数 x をとり、その2乗である x * x を値として戻すという意味だ。

理論的には難解でも、操作的に理解するとそれほど難しくは感じない。Haskell は勉強しようとせずに使い方を考えると急に易しくなる。

4.4.1 Lambda Calculus

ここではラムダ計算について解説してある。ラムダ式は λx.x*x のように書くこれは引き数を2乗する関数 f(x) = x * x を表している。λはラムダ抽象と呼ばれ、変数 x が束縛変数であることを表す。(λx.x*x) 5 は関数 (λx.x*x) の束縛変数 x に数値 5 を代入して関数の値を計算することを表す。これを (λx.x*x) を 5 に関数適用するという。このばあい x = 5 を x*x に代入するので関数適用の結果は25だ。

ラムダ式は基本的に一つのパラメータ(束縛変数)しかとらない。パラメータが x y と2つあるときは、y をラムダ抽象した式 λy.2*x+y について、さらに x をラムダ抽象するという風に考えて λx(λy.2*x+y) と書くが、計算の順番が明らかなときは括弧を省略して λxλy.2*x+y と書く。λ式にはパラメータがひとつしか無いという考え方は、関数の一部のパラメータに値を代入してパラメータの数を減らすカリー化という手続きに活用される。

ごちゃごちゃと訳したが、ラムダ計算、ラムダ抽象、パラメータ、関数適用、カリー化などの用語を厳密ではなくても感覚的に使いこなせるようになると便利だ。

Haskell ではλ記号はバックスラッシュで代用するので次のようになる。

Prelude> (\x -> x * x) 5
25
Prelude> (\x y -> 2 * x + y) 2 3
7

要するに、プログラムが書ければOKの立場からは、ラムダ計算は無名関数の書き方だと割りきっても構わないし、そのイメージのほうが分かりやすい。

4.4.2 Higher Order Types

関数の型のことを higher order type という。Higer order type の訳語を検索したが分からなかった、高階型とでもいうのだろうか。ラムダ式の変数が1個の場合、関数の型は引き数の型と関数の値(戻値)の型で表される。

Prelude> let
Prelude| square :: Int -> Int
Prelude| square = \x -> x * x
Prelude|
Prelude> square 2
4
Prelude> :t square
square :: Int -> Int

引き数が2つ以上ある場合も考えてみる。ラムダ式のパラメータは原則1個なので、(/x -> (/y -> 2*x+y)) の場合は、2*x+y の y にラムダ抽象してできた式 (/y -> 2*x +y) に x をラムダ抽象するという形になる。2*x+y の x y にそれぞれ Int 型の数を代入して得られる値(戻値) Int 型になるので、f = (\x -> (\y -> 2*x+y)) の型は Int -> (Int -> Int) になる。

Prelude> let
Prelude| f :: Int -> (Int -> Int)
Prelude| f = (\x -> (\y -> 2*x+y))
Prelude|
Prelude> f 2 3
7
Prelude> :t f
f :: Int -> Int -> Int

上の f で見られるように関数の型が Int -> (Int -> Int) のように順番にラムダ抽象化されている場合は括弧を外すことができる。しかし、(Int -> Int) -> Int のような場合は括弧は外せない。

関数の型を上のように必ずパラメータ1個の関数と考えて表現するやり方は、面倒だが、全てのラムダ式をパラメータ(引数)1個の関数に分解して考えるやり方は、IOモナドを取り扱うときに重要になってくる。

上の説明は少々形式的になったが、今の段階では、関数の型は単に引数の型と値の型を -> で繋いだものと考えても構わないし、分かりやすい。

Prelude> let
Prelude| f2 :: Int -> Int -> Int
Prelude| f2 = \x y -> x + y
Prelude|
Prelude> f2 1 2
3
Prelude> :t f2
f2 :: Int -> Int -> Int

上の関数は変数の型が決まっている場合だったが、多相型を取り扱える関数もある。例えば (+) は Int 型も Double 型も扱えるので型の表示は次のように、型変数を用いたものになる。

Prelude> :t (+)
(+) :: Num a => a -> a -> a

4.4.2 節にはそのような関数の例がいくつか挙げてある。

Prelude> :t head
head :: [a] -> a
Prelude> :t tail
tail :: [a] -> [a]
Prelude> :t null
null :: [a] -> Bool
Prelude> :t fst
fst :: (a,b) -> a
Prelude> :t snd
snd :: (a,b) -> b

また、中置演算子を () で囲むことで、その型を表示できる。

Prelude> :t (+)
(+) :: Num a => a -> a -> a
Prelude> :t (*)
(*) :: Num a => a -> a -> a
Prelude> :t (++)
(++) :: [a] -> [a] -> [a]
Prelude> :t (:)
(:) :: a -> [a] -> [a]

4.4.3 That Pesky IO Type

ここでは、IO () や IO String などの IO モナドの関数の値(戻値)の型について説明してある。次のように IO モナドの関数の戻値は必ず IO a 型になる。

Prelude> :t getLine
getLine :: IO String
Prelude> :t putStrLn
putStrLn :: String -> IO ()

IO a 型は "IO アクション” と呼ばれている。IO モナドを調べている時に "IO アクション" が何を指しているのか分からず混乱したことがあった。Read World Haskell を読んでそれが IO a 型の値であることが分かったが、ここにもはっきりと書いてあった。

IOアクションを返す関数は、do 記法の中で使われなくてはならない。また、IO a から a を取り出すためには <- (arrow convention) を使わなければならない。

Prelude> do
Prelude| cs <- getLine
Prelude| putStrLn cs
Prelude|
hello, world
hello, world

YAHT の次の例は概念的なものだが、IOモナドの基本的な使い方を集約してある。つまり、IOモナドの関数は do 記法の中に記述すること、IOモナドの中で純粋関数を利用するためには in を伴わない let 文の中で行うことなどだ。

main = do
  s <- readFile "somefile"
  let i = f s
  putStrLn (show i)

4.4.4 Explicit Type Declarations

ここでは、明示的に関数の型を宣言する方法が説明してある。明示的に関数の型を宣言することは必須ではないが、Clarity (分かりやすさ)、Speed (実行速度)、Debugging (デバッグのしやすさ) の点で有利だと書いてある。

次の例はタイプクラスを利用する場合の型宣言法。

square :: Num a => a -> a
square x = x*x

宣言された型 Num a => a -> a は type signature (訳語が分からなかった) と呼ばれる。

引数の型が確定している場合。

square :: Int -> Int
square x = x*x

ghci で -fglasgow-exts オプションが設定されている場合、関数の型宣言ではなく式の中で型を指定できる。

square (x :: Int) = x*x

ラムダ記法の中で型を指定する場合。

Prelude> :t ((\x -> x*x) :: Int -> Int)
((\x -> x*x) :: Int -> Int) :: Int -> Int

4.4.5 Functional Arguments

ここでは、高階関数の関数型について述べてある。高階関数は関数を引数に取るので、引数の型が関数型になる。本文とは内容が異なるが、filter と foldr の型を次に表示した。

Prelude> :t filter
filter :: (a -> Bool) -> [a] -> [a]
Prelude> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b

filter も foldr もよく使う関数なので使い慣れてから上の関数型を読んだほうが分かりやすいだろう。

Yet Another Haskell Tutorial 5 に続く ...
by tnomura9 | 2013-01-29 12:55 | Haskell | Comments(0)
<< Yet Another Has... Yet Another Has... >>