関数

【Haskell】foldl関数、foldr関数の基本的な使い方 ~畳み込み~

【Haskell】foldl関数、foldr関数の基本的な使い方 ~畳み込み~

Haskellで畳み込み計算をするためのfoldl関数とfoldr関数の使い方を解説します。

畳み込み関数 (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関数
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b

foldl関数は、以下の3つの引数を取ります。

  1. 関数 (b -> a -> b):リストの各要素と累積値に適用され、新しい累積値を生成します。例えば、(+)は2つの数を受け取り、その和を返す2項関数です。
  2. 初期値 b:畳み込みを開始するための値です。例えば、数値リストを畳み込む場合の初期値として0がよく使用されます。
  3. 畳み込まれるリスト t a:Foldable型クラスのインスタンスであり、畳み込み対象となるデータです。例えば、Intのリスト([Int])等が該当します。

ここで、Foldableというのは型クラスを表しています。tFoldable型クラスのインスタンスである必要があります。今回は簡単のため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(+)z0、xsを[1, 2, 3, 4, 5]と考えてどのような動きをしているか考えてみましょう。具体的にどのように処理が流れていくかを記載してみると以下のようになります。

foldl関数の説明

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関数
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

foldl関数と非常に似た型シグネチャであることが分かるかと思いますが、違いとしては第1引数で受け取る関数の型が(a -> b -> b)となっていて、abが逆転しています。これは、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(+)z0、xsを[1, 2, 3, 4, 5]と考えてどのような動きをしているか考えてみましょう。具体的にどのように処理が流れていくかを記載してみると以下のようになります。

foldr関数の説明

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にとっては代表的で重要な関数です。しっかりと動作を理解して使いこなせるようにしていただきたいと思います。