理想未来ってなんやねん

娘可愛い。お父さん頑張る。

Haskell 16日目 〜 関数合成と部分適用 〜

Haskell 16日目。

関数合成

関数合成(function composition)とは、複数の関数を合成して新しい関数を作ることです。

例えば、文字列に含まれる行数を数える関数、numberOfLinesは$演算子を使って記述すると次のように書けます。

numberOfLines :: String -> Int
numberOfLines cs = length $ lines cs

上記の関数は関数合成を使うと次のようにも書けます。

numberOfLines :: String -> Int
numberOfLines = length . lines

(.)関数は、2つの関数を合成する関数です。
『合成する』とは、『2つの関数を順番に適用する新しい関数を作る』ということで、例えば(length . lines)は、『引数にlines関数を適用し、その値にlength関数を適用する関数』となります。

部分適用

部分適用とは

Haskellでは関数にすべての引数を同時に渡さなくとも構わないようになっています。
例えば、引数を3つとる関数に1つだけ引数を渡しておいて、残りの2つをあとから渡すことができるのです。
そのように、引数を一度にすべて渡さず、一部だけ渡しておくことを部分適用(partical application)と言います。

部分適用の例として、次のaddThree関数があったとします。

addThree::Int -> Int -> Int -> Int
addThree i j k = i + j + k

関数の意味は考えず、引数の数にだけ注目します。addThree関数は3つの引数をとる関数です。
そこで、あえて引数を1つだけ渡してみます。

addThree 5

このように相手も『引数が足りない』などというエラーにはなりません。これはHaskellでは正しい式なのです。
(addThree 5)の値は、『addThree関数の第1引数に5を渡して引数を1つ減らした関数』となり、以下の無名関数と同じです。

\j k -> 5 + j + k

(addThree 5)の値である関数に、さらに引数を渡してみます。

(addThree 5) 6

((addThree 5) 6)の値は以下の無名関数を同じです。

\k -> 5 + 6 + k

最後に((addThree 5) 6)にもう1つ引数を渡してみます。

((addThree 5) 6) 7

(((addThree 5) 6) 7)の値は以下のようになります。

5 + 6 + 7

つまり、(((addThree 5) 6) 7)の値は18です。この結果は(addThree 5 6 7)のように引数をまとめて書いた場合と変わりません。

Haskellでの関数適用

Haskellでは(addThree 5 6 7)のようにすべての引数を一度に書いた場合でも、部分適用を使って(((addThree 5) 6) 7)と書いているのだと解釈されます。つまり(take 3 [1, 2, 3, 4])と買いても((take 3) [1, 2, 3, 4])と解釈され、(replicate 5 'a')と書いても((replicate 5) 'a')と解釈されています。
Haskellには真の意味で2つ以上の引数をとる関数は無く、関数はどれも1つだけ引数をとって関数を返す関数となります。
関数を返す関数は高階関数となりますので、Haskellでは2引数以上の関数はどれも高階関数ということになります。

セクション

2項演算子に対しても部分適用を使うことができます。部分適用を使い引数を1つだけ渡した2項演算子のことを特にセクション(section)と言い、普通の部分適用とは特殊な機能が用意されています。

以下の関数の例を見てみます。

increment :: Int -> Int
increment n = n + 1

incrementは引数に1を足す関数です。この関数はセクションを使うと次のようにも書けます。

increment :: Int -> Int
increment n = (+ 1) n

『(+ 1)』がセクションです。数値のプラス1ではありません。
なお、括弧も含めてセクションの為、括弧を省略する事はできません。

また、(+ 1)は演算子の左側を渡していないセクションでしたが、その逆に、演算子の右側を渡していないセクションも作れます。

increment :: Int -> Int
increment n = (1 +) n

『(1 +)』がセクションです。今回も括弧は省略できません。
関数(1 + n)に引数としてnを与えて『(1 +) n』とすると、『1 + n』の値が得られます。

セクションと(-)関数

Haskellには単項演算子の『+』はありませんが単項演算子の『-』はあります。
そのため、『(-1)』が果たして数値『-1』なのか、それとも(n - 1)の左辺を渡していないセクションなのか、見た目からは明白でないということになってしまいました。
Haskellでは、(-1)は常に数値『-1』と解釈されます。(- 1)のように数字の間にスペースを入れても、やはり数値として解釈されます。(n - 1)の左辺を渡していないセクションに対応する関数を作るには、subtract関数を使って(subtract 1)と書く必要があります。

部分適用の応用(1) ----高階関数と部分適用

部分適用の典型的な応用例を見ていきます。
まず、高階関数と部分適用を組み合わせる使い方です。次の例はmap関数とセクションを使ってリストの全要素に7を足す関数の例です。

map (+ 7) [1, 2, 3, 4, 5]

(+ 7)がセクションで、『引数に7を足す関数』です。その関数をmap関数でリストの各要素に適用するので、上記の式はリストの全要素に7を足すことになります。その値は[8, 9, 10, 11, 12]です。

次はfilter関数を使って、文字列から'\r'だけを取り除く例です。

filter (\= '\r') "aaa\r\nbbb\r\nccc\r\nddd\r\neee\r\n"

『\=』は『==』の否定です。CやJavaでいう『!=』に当たります。
(\= '\r')は『引数が'\r'でなければTrueを返す関数』となり、『filter (\= '\r') cs』はcsのうち'\r'以外の文字を抜き出します。逆に言うと、csから'\r'だけを取り除きます。

部分適用の応用(2) ----変数の削減

次は部分適用を使って定義から変数を削減する例を見ていきます。

次の関数の例

zipLineNumber :: [String] -> [(Int, String))]
zipLineNumber xs = zip [1..] xs

この関数は、部分適用を使うと次のように書き換えられます。

zipLineNumber :: [String] -> [(Int, String))]
zipLineNumber = zip [1..]

このように書き換えられる理由は、次のように定義を変形すると分かりやすくなります。

zipLineNumber xs = zip [1..] xs
            ↓
zipLineNumber xs = (zip [1..]) xs
            ↓
zipLineNumber xs = f xs  where f = zip [1..]

こう書き換えてみると、zipLineNumber関数は関数fの別名にすぎないことが分かります。
次のように書き換えてみます。

zipLineNumber xs = f xs
            ↓
zipLineNumber = f

ここでfをzip [1..]に展開すれば、最初の形になります。

zipLineNumber = zip [1..]


今日のところはここまで。