Haskellで畳み込み計算をするためのfoldl
関数とfoldr
関数の使い方を解説します。
Contents
畳み込み関数 (foldl, foldr)
畳み込みとは、適用する関数、初期値、畳み込み可能な型のデータ(例えばリスト)を受け取って単一の値にする計算のことを言います。Haskellでは、畳み込み関数の代表的なものとしてfoldl
関数とfoldr
関数があります。
これらの関数において、l
は「left」、r
は「right」を表しており、foldl
は左畳み込み、foldr
は右畳み込みと言います。これらは左から値を畳み込んでいくのか、右から値を畳み込んでいくのかの違いがあります。
foldl
関数やfoldr
関数のように引数に関数を受け取る関数は、高階関数と言います。高階関数の概要については「高階関数の基本」でまとめているので参考にしてください。畳み込み関数(foldl
, foldr
)は、高階関数の中でも代表的な関数であり、関数型プログラミングにおいて中心的な関数の一つです。他のプログラミング言語では、reduce
という関数名の場合もあります。
この記事では、畳み込み計算をするためのfoldl
関数とfoldr
関数の使い方を解説します。
関数型プログラミングの中心的な関数としては他にもmap
関数やfilter
関数があります。以下のページでまとめていますので参考にしてください。
foldl関数の基本的な使い方
foldl
関数は、Haskellにおいてリスト等のFoldable
な型を畳み込むために使用します。「l
」は「left」を表しており、左畳み込みとも言われます。
foldl
関数の型シグネチャは以下のようになっています。
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldl
関数は、以下の3つの引数を取ります。
- 関数
(b -> a -> b)
:リストの各要素と累積値に適用され、新しい累積値を生成します。例えば、(+)
は2つの数を受け取り、その和を返す2項関数です。 - 初期値
b
:畳み込みを開始するための値です。例えば、数値リストを畳み込む場合の初期値として0
がよく使用されます。 - 畳み込まれるリスト
t a
:Foldable型クラスのインスタンスであり、畳み込み対象となるデータです。例えば、Int
のリスト([Int]
)等が該当します。
ここで、Foldable
というのは型クラスを表しています。t
はFoldable
型クラスのインスタンスである必要があります。今回は簡単のためFoldable
のインスタンスであるリストを使った例を紹介しますが、他の型でもfoldl
関数は使用できます。他のFoldable
型クラスのインスタンスである型としてはTree
型やMaybe
型などが該当します。
foldl関数の使用例
数値の畳み込み
foldl
関数を使用して、数値のリストを畳み込む場合には以下のようにします。
-- 数値の畳み込み
plusFoldl :: [Int] -> Int
plusFoldl xs = foldl (+) 0 xs
【実行結果例】
ghci> plusFoldl [1, 2, 3, 4, 5]
15
上記で定義したplusFoldl
関数は、foldl
を使用してリストxs
の中の全ての数値を左から順に加算(+)
し、その合計値を返します。左から順に足しこんでいく基準の初期値は0
となります。実行結果例をわかる通り[1, 2, 3, 4, 5]
を引数に渡すと結果は15
となります。
foldl関数の動作を理解する
上記の例を見てfoldl
関数がどのような結果となるかは分かったかと思いますが、より関数の動作を理解するには自分で実装するとどうなるかを考えてみることが非常に役に立ちます。
foldl
関数は、型シグネチャを見るとわかる通りリスト限定のものではありませんが、リストを前提として左畳み込みを自分で実装してみる場合のmyFoldl
関数は以下のように実装することができます。
-- foldlを自分で実装する
myFoldl :: (b -> a -> b) -> b -> [a] -> b
myFoldl f z [] = z
myFoldl f z (x:xs) = myFoldl f (f z x) xs
上記の関数においてf
を(+)
、z
を0
、xsを[1, 2, 3, 4, 5]
と考えてどのような動きをしているか考えてみましょう。具体的にどのように処理が流れていくかを記載してみると以下のようになります。
myFoldl
関数は、再帰的に定義されています。「myFoldl f z (x:xs) = myFoldl f (f z x) xs
」の部分に注目してみると、初期値0
とリストの先頭要素1
に対して引数で与えられた関数(+)
を適用し、その結果が次の初期値となっています。このように順に計算をしていくと計算結果の15
が得られます。具体的には(((((0 + 1) + 2) + 3) + 4) + 5) = 15
のように計算されていることが分かるかと思います。
foldr関数の基本的な使い方
foldr
関数も、foldl
関数と同様にリスト等のFoldable
な型を畳み込むために使用します。「r
」は「right」を表しており、右畳み込みとも言われます。
foldr
関数の型シグネチャは以下のようになっています。
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
foldl
関数と非常に似た型シグネチャであることが分かるかと思いますが、違いとしては第1引数で受け取る関数の型が(a -> b -> b)
となっていて、a
とb
が逆転しています。これは、foldr
関数がリストの要素を右から畳み込んでいくことを意味しています。
foldr関数の使用例
数値の畳み込み
foldl
関数と同じようにfoldr
関数を用いて数値の畳み込みをする例を見てみましょう。
-- 数値の畳み込み
plusFoldr :: [Int] -> Int
plusFoldr xs = foldr (+) 0 xs
【実行結果例】
ghci> plusFoldr [1, 2, 3, 4, 5]
15
上記で定義したplusFoldr
関数は、foldr
を使用してリストxs
の中の全ての数値を右から順に加算(+)
し、その合計値を返します。右から順に足しこんでいく基準となる初期値は0
となります。実行結果例をわかる通り、[1, 2, 3, 4, 5]
を引数に渡すと結果は15となります。
foldr関数の動作を理解する
数値を畳み込む例を見てきましたが結果としてはfoldl
関数とfoldr
関数では同じ結果になりました。しかし、内部的には全く異なる動作をしています。
foldl
関数でも見てきたようにfoldr
関数についても、自分で実装してみることにしてみましょう。foldr
関数をリストに対して実装したmyFoldr
関数は以下のようになります。
-- foldrを自分で実装する
myFoldr :: (a -> b -> b) -> b -> [a] -> b
myFoldr _ z [] = z
myFoldr f z (x:xs) = f x (myFoldr f z xs)
上記の関数においてf
を(+)
、z
を0
、xsを[1, 2, 3, 4, 5]
と考えてどのような動きをしているか考えてみましょう。具体的にどのように処理が流れていくかを記載してみると以下のようになります。
myFoldr
関数についても再帰的に定義されています。「myFoldr f z (x:xs) = f x (myFoldr f z xs)
」の部分について着目してみると、リストの先頭要素が前に来て、初期値が後ろにスライドし、残りのリストを使って再度myFoldr
が適用されていることが分かるかと思います。
このように書き下していくと最終的には「myfoldr (+) 0 []
」という形となり、これは「myFoldr _ z [] = z
」の定義より初期値z
、今回の例では0
になります。最終的には、(1 + (2 + (3 + (4 + (5+0)))))
のように右から(+)で畳み込まれている計算となっていることが分かるかと思います。
上記で見てきた通りfoldl
関数とfoldr
関数は、左から畳み込むのか、右から畳み込むのかという点で具体的な動作が異なっています。
foldl関数とfoldr関数の違い
適用する関数による結果の違い
上記まででfoldl
関数とfoldr
関数を見てきました。足し算(+)
を例に見る限りでは、結果は同じでした。しかし、各関数の動作を理解するために動作をステップごとに書いてみましたが「左から畳み込むのか」「右から畳み込むのか」という違いがありました。
例えば、適用する関数として引き算(-)
を指定した場合には、以下のように結果が大きく変わります。
-- foldlの場合
minusFoldl :: [Int] -> Int
minusFoldl xs = foldl (-) 0 xs
-- foldrの場合
minusFoldr :: [Int] -> Int
minusFoldr xs = foldr (-) 0 xs
【実行結果例】
ghci> minusFoldl [1, 2, 3, 4, 5]
-15
ghci> minusFoldr [1, 2, 3, 4, 5]
3
上記の例についても具体的に書き下してみると以下のような計算になっていることが分かります。
minusFoldl
の場合:(((((0 - 1) - 2) - 3) - 4) - 5) = -15
minusFoldr
の場合:(1 - (2 - (3 - (4 - (5 - 0))))) = 3
上記のように畳み込む方向が左からから、右からかという違いがあるため、適用する関数によって大きな違いが出ることに注意が必要です。
無限リストに対する挙動の違い
Haskellは遅延評価言語であるため、foldr
は無限リストに対しても使用することが可能な場合があります。これは、結果を得るためにリスト全体を評価する必要がないケースで利点となります。
一方で、foldl
は基本的にリストの全体を評価しようとするため、無限リストには適していません。無限リストに対してfoldl
を適用すると無限ループとなってしまいます。
まとめ
Haskellで畳み込み計算をするためのfoldl
関数とfoldr
関数の使い方を解説しました。
畳み込みとは、適用する関数、初期値、畳み込み可能な型のデータ(例えばリスト)を受け取って単一の値にする計算のことを言います。foldl
関数は左畳み込み、foldr
関数は右畳み込みといい、値を畳み込む順序が異なっています。
この記事では、foldl
関数とfoldr
関数の各関数の概要と基本的な使い方、そして違いについて説明しました。
畳み込み関数(foldl
, foldr
)は、map
関数やfilter
関数と同様に関数型プログラミングのHaskellにとっては代表的で重要な関数です。しっかりと動作を理解して使いこなせるようにしていただきたいと思います。