あどけない話

インターネットに関する技術的な話など

関数型言語での関数の基礎知識

関数型言語での関数について、Haskell を用いて説明します。

関数の型

関数の型は、-> を使って書きます。例えば、Int を Char に変換する chr という関数の型は、以下のようになります。

chr :: Int -> Char

一引数の関数の型は、まぁこんなもんだと思えるでしょう。びっくりするのは、引数が増えたときです。たとえば、replicate という関数の型を見てみましょう。

replicate :: Int -> a -> [a]

replicate は、第二引数で指定されたデータを第ー引数に指定された個数分用意して、リストにして返す関数です。([] は配列ではなく、リストです。) 次のように動きます。

> replicate 3 "foo"
→ ["foo","foo","foo"]

a は型変数といって、任意の型を取れます。なじめない人は、具体的な型を当てはめてみるといいでしょう。たとえば、a を String に固定してやると、以下のような型になります。

replicate :: Int -> String -> [String]

びっくりする点は、第一引数と第二引数の間にも -> が使われていることです。命令型言語の人は、こんな感じに書くべきだと思うのではないでしょうか? (以下の丸括弧は、タプルという無名の構造体を表します。)

replicate :: (Int, String) -> [String]

これを理解するには、カリー化を学ぶ必要があります。

カリー化

関数型言語の関数は、カリー化の概念を用いいて定義されています。その意味するところは、関数が一つの値を取って一つの値を返す形で定義されるということです。関数が一つの引数しか取れないなら、どうやって複数の引数を渡せばいいのでしょうか?

その答えは、関数が「値」の他に「関数」も返せるようにすればいいのです。実は、-> という記号は、右結合なので以下のように括弧を補うことができます。(以下の丸括弧は、結合力を示すための丸括弧です。)

replicate :: Int -> (String -> [String])

こうすれば、replicate の型の意味がはっきりします。すなわち、replicate は、『Int を取って「String -> [String] という型を持つ関数」を返す関数』なのです。

実際、すべての引数を与える必要はなく、左の引数のいくつかを与えることができます。

replicate3 :: String -> [String]
replicate3 = replicate 3

一部の引数を与えることを「部分適用」といいます。カリー化の概念を用いて定義されているので、本当は部分じゃないんですが、初心者にはこういう言い方が分かり易いので、この用語がまかり通っています。

クロージャの概念を知っている人なら、これがクロージャであることがはっきり分かるでしょう。ただし、クロージャ内の変数は変更できません。Haskell では、クロージャクロージャと意識せずに使えるところが素晴らしいのです。

カリー化されている?

「カリー化」という用語は、二つの意味で使われることがあるので、注意が必要です。

  1. 関数がカリー化の概念を用いて定義されることを「関数がカリー化されている」という人がいます。
  2. Haskellでの本当のカリー化とは、引数に構造体を取る関数をカリー化の概念を用いて定義し直すことです。

追記:カリー化談義を読んで下さい。

後者の意味での、カリー化を実現する関数は curry と言います。

curry :: ((a, b) -> c) -> a -> b -> c
curry f x y = f (x,y)

(a, b) -> c のようにタプルを取る関数を a -> b -> c という型を持つ関数に再定義していますね。

この逆をするのが uncurry です。

uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f (x,y) = f x y

無名関数

無名関数は、バックスラッシュの後に引数を書き、-> を続けて、その後に本体を書きます。

\x -> x + 5

以下のような二引数の関数(厳密な人には怒られる表現!)を考えましょう。

calc :: Int -> Int -> Int
calc x y = x + y - 1

calc の第二引数を右側に移項(?)すると、こう書けます。

calc x = \y -> x + y - 1

(右辺にだけ見ると、x が自由変数であることがはっきり分かりますね。) さらに、第一引数を移項してみましょう。

calc = \x -> \y -> x + y - 1

よくできていると、思いませんか?
結局、

calc x y = x + y - 1

という関数定義の書式は、

calc = \x -> \y -> x + y - 1

の構文糖衣に過ぎないのです。

なお、この定義は構文糖衣を使って以下のようにも書けます。

calc = \x y -> x + y - 1

演算子と関数

Haskell では、二引数の関数をバッククオートで囲むと、二項演算子のように真ん中に書けます。

たとえば、

isPrefixOf "foo" "foobar"

"foo" `isPrefixOf` "foobar"

と書けます。英語の語順で読めて便利でしょ?

逆に二項演算子は、丸括弧で囲むことで、関数のように先頭に書くことができます。

たとえば、

3 + 4

(+) 3 4

と書けます。

データ構成子と関数

Haskell でのデータは以下のように定義します。

data Maybe a = Nothing | Just a

ここで、Maybe は型の名前です。また、Nothing や Just は、データ構成子と呼ばれています。データの種類を表すタグ名だと考えてもよいでしょう。

驚くべきことに、データ構成子は関数のように扱えます。実際、Just は以下のような型を持ちます。

Just :: a -> Maybe a

以下は、Just をあたかも関数のように使う例です。

> map Just [1,2,3]
→ [Just 1,Just 2,Just 3]

関数合成

一引数の関数は、演算子(.)で合成することができます。

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

最初は分かりにくいかもしれないので、部分式の型を調べてみましょう。

f :: b -> c
g :: a -> b
x :: a
g x :: b
f (g x) :: c

分かりましたか?

(.) の型に丸括弧を付けてみましょう。

(.) :: (b -> c) -> (a -> b) -> (a -> c)

b -> c と a -> b を合成して a -> c という型を持つ関数を返しているのがはっきり分かります。

(.) の利用例を以下に示します。

add1 :: Int -> Int
add1 x = x + 1
next :: Char -> Char
next = chr . add1 . ord

ord は、Int -> Char という型を持つ関数です。そこで next は、文字を取り、その次の文字を返す関数になります。

> next 'a''b'

念のため、それぞれの型を書いてみます。

ord :: Char -> Int
add1 :: Int -> Int
chr :: Int -> Char

(.) が結合できるのは、一引数の関数だけなので、用途が狭いと思う人もいるでしょう。しかし、Haskell の関数は、部分適用できることを思い出して下さい。いくらでも、一引数の関数を作り出すことができます。(ああ、怒られそうな説明だ!) 以下の例はそれをはっきりさせると思います。

next :: Char -> Char
next = chr . (+) 1 . ord
-- 演算子を関数にして部分適用

おまけ:今回はセクションを説明しませんでしたが、セクションを使えば、以下のようにも書けます。

next :: Char -> Char
next = ord . (+1) . chr

合わせて読みたい