🐳

高階関数の読み解き方を学ぼう(Haskell)

2022/05/24に公開

この記事では高階関数の読み取り方を学びます。Haskellでは引数/戻り値が区別されないので少し見づらいですよね。引数を2つ足す関数/3つ足す関数を例に、その読み解き方をマスターしたいと思います。

引数を2つ足す関数(addtwo関数)

引数を2つ足し算する関数を例に考えます。addtwoという名前の関数です。10、20という引数を2つ渡して、30という計算結果を得ることができます。『引数を2つ取って10,20を足し算する関数』をイメージしながら高階関数を作っていきたいと思います。

addtwo :: Int -> Int -> Int
addtwo a b = a + b
    
main = print (addtwo 10 20)
-- 実行結果「30」

型の定義を3パターン

型の定義を3パターン用意してみました。以下3つの関数型にはどのような違いがあるのでしょうか。addtwo関数を例に型の定義について考えていきたいと思います。

addtwo1 :: Int -> Int -> Int
addtwo2 :: Int -> (Int -> Int)
addtwo3 :: (Int -> Int) -> Int

addtwo1

addtwo1 :: Int -> Int -> Int

引数を1つずつ受け取る

引数が2つ定義されていても同時に処理することはありません。引数は一度に1つしか受け取れない点に注意して下さい。そして1つの引数を受け取った後、矢印->の右側にあるモノは全て返却されます。

まず左側にあるIntを1つ受け取ります。その結果として右側にあるInt -> Intが返却されます。Int -> Intは更に引数を1つとって、処理結果として戻り値を返却します。わかりづらいので10、20という数値を使った場合の処理は以下の流れです。

(1)addtwo関数が10へ適用される
(2)数値を返却する代わりに、新たに別の関数を返却する
 (新たに作られたこの関数は引数を1つとって、10と足し算する)
(3)新たに作られた別の関数が20へ適用される
→10+20の計算結果として30が返却される

矢印->とは?

Int -> Intとは何でしょうか。型の定義に矢印->が含まれています。関数型の定義にはこの矢印->が含まれます。矢印->の記号はある型から別の型へ変換することを意味します。型の定義に矢印->を含むモノは全て関数です。

Int -> Intとは「Int型のデータを受け取って、Int型のデータを返却する」ことを表します。型の定義だけでは値を内部でどのように処理するのかわかりません。 とりあえずInt型の値を受け取った結果としてInt型の値を返却することがわかります。

addtwo2

addtwo2はどのように動作するのでしょうか。addtwo1とは違って関数型(型 -> 型)がある点に注目して下さい。

addtwo2 :: Int -> (Int -> Int)

まず1番左側にあるIntを引数として1つ受け取ります。そして右側にあるモノを戻り値として全て返却します。ここでは(Int -> Int)を返却します。矢印->が含まれているのでこれは関数です。つまりInt型の引数を受け取って、更にまた関数を返却します。

この関数の処理というのも、Int型の引数を受け取ります。そして最終的にInt型の戻り値が返ってきます。結局はaddtwo1と同じことをしています。しかも()で囲まれているので『内部的には関数を返却しているんだな..』ということが一目でわかります。同じように引数を2つ渡しても同じ結果が出てきます。

addtwo2 :: Int -> (Int -> Int)
addtwo2 a b = a + b
    
main = print (addtwo2 10 20)
-- 実行結果「30」

addtwo2の戻り値の型/引数としての関数の戻り値

戻り値を返却する部分でよく混乱しがちです。以下のaddtwo2'は動作しません。関数を返却するとあったので関数を引数に渡したのですが..なぜでしょうか。(処理としては値:aと関数:fを受け取り、faへ適用しています。)

addtwo2' :: Int -> (Int -> Int)
addtwo2' a f = f a

main = print $ addtwo2 10 (+20)
-- 実行結果「エラー」

なぜなら1番最後にあるデータ型は関数それ自体の戻り値の型を指定するからです。1番最後にあるIntはaddtwo2'関数の戻り値の型であり、独立した別の関数の戻り値ではありません。 そしてその直前にあるIntはaddtow2'関数への引数となります。

関数それ自体(ここではaddtwo2')の戻り値/引数としての関数の戻り値 は明確に異なります。 関数定義の最後にあるデータ型を()で囲んだ場合、それは引数として受け取る(独立した別の)関数ではないので注意して下さい。

addtwo3

addtwo3の場合はどうでしょうか。こちらの場合は少し作り方が特殊です。1番最後のIntはaddtwo3関数の戻り値の型として既に埋まっています。なのでaddtwo3関数へは関数しか渡すことができません。

addtwo3 :: (Int -> Int) -> Int

順番に読み解いていきます。()の中にあるモノは一まとまりとして扱われ、()内では矢印->を型の定義に含んでいます。矢印->を型の定義に含むモノは関数です。つまり()内で型として指定されているのは引数ではなく、関数です。正確にいえば引数を1つ取る関数です。

引数を1つ取る関数とは?(+10)

引数を1つ取る関数とは何でしょうか。例えば(+10)などはどうでしょうか。2項演算子は引数を2つ取る必要がありますが、片方だけ渡します。そうすれば引数を1つ取る関数を作ることができます。(+10)とは『引数を1つとって10と足し算する関数』ということになります。

引数として受け取れるのは関数のみ

(+10)を使って実装したのが以下です。引数は関数しか受け取ることができません。なので(+10)のみを引数して渡しました。

addtwo3 :: (Int -> Int) -> Int
addtwo3 f = f 20

main = print $ addtwo3 (+10)

2行目:処理内容の定義に注目して下さい。(+10)という関数を20という値へ適用しています。引数として値を渡すことはできないので(関数が指定されている&1番最後のIntはaddtwo3関数の戻り値だから)関数の内部で加算の処理をすることにしました。

10を足し算する関数

(+10)という関数は部分適用の仕組みを使っているので少し特殊ですよね。plus_ten関数として実装し直したのが以下となります。

addtwo3 :: (Int -> Int) -> Int
addtwo3 f = f 20

plus_ten :: Int -> Int
plus_ten a = a + 10

main = print $ addtwo3 plus_ten

いずれにしろ引数を関数のみ渡している点に注目して下さい。高階関数を作る場合、戻り値の扱いには注意が必要です。関数それ自体(ここではaddtwo3)の戻り値の型/引数としての関数の戻り値 は明確に異なります。

引数を3つ足す関数(addthree関数)

では次に引数を3つ足すaddthree関数を例に考えてみます。動作としては以下をイメージしています。

addthree :: Int -> Int -> Int -> Int
addthree a b c = a + b + c
    
main = print $ addthree 10 20 30
-- 実行結果「60」

関数の型を定義するには()も含めると6つのパターンが考えられます。以下を例に高階関数についてもう少し考えてみます。

addthree1 :: Int -> Int -> Int -> Int
addthree2 :: (Int -> Int) -> Int -> Int
addthree3 :: Int -> (Int -> Int) -> Int
addthree4 :: Int -> Int -> (Int -> Int)
addthree5 :: (Int -> Int -> Int) -> Int
addthree6 :: Int -> (Int -> Int -> Int)

addthree1

まずは()がないaddthree1関数からです。以下の2つのルールに注意しながら読み解いていきます。

1、矢印->の左側にあるモノを1つ引数に取り、右側にあるモノを全て返却する
2、矢印->を型の定義に含むモノは全て関数である
(なぜなら「ある型から別の型へ変換する型」という機能を持っているから)

addthree1 :: Int -> Int -> Int -> Int

まず1番左側のIntを引数として受け取り、それ以降のInt -> Int -> Intを戻り値として返却します。Int -> Int -> Intとは何でしょうか。ここでも引数を1つしか受け取れないことに注目して下さい。つまりInt -> Int -> IntとはIntを受け取り、Int -> Intを返却する関数です。

()は特に書かれていないので、単純に引数を1つずつ受け取る関数です。3つのIntを1つずつ引数として受け取って、4つ目のIntを戻り値として返却します。右側にあるモノを全て返却しますが、ここでも引数を1つ受け取る関数ができますので注意して下さい。

addthree2

addthree2関数には(Int -> Int)という箇所があります。(型 -> 型)とは関数型の定義であることを思い出して下さい。そして1番最後のIntは戻り値です。なのでaddthree2関数へは(1)1引数の関数、(2)Int型の値 の2つを引数として渡します。

addthree2 :: (Int -> Int) -> Int -> Int

ややこしいですが以下のように実装して10+20+30を実現します(こんな関数を使う場面はないと思いますが、高階関数の型の定義を理解するためなので悪しからず..)

addthree2 :: (Int -> Int) -> Int -> Int
addthree2 f a = f a + 30

plus_twenty :: Int -> Int
plus_twenty b = b + 20

main = print $ addthree2 plus_twenty 10
-- 実行結果「60」

1引数の関数としてplus_twentyを新たに作りました。plus_twentyは引数を1つ(ここでは10)を受け取って、それを20と足します。引数の1つ目(=1引数の関数)として、plus_twentyaddthree2へ渡します。(引数の2つ目にはInt型の値が必要なので、10という値を渡しました。)

2行目のf a + 30という部分で処理を定義しています。plus_twenty10へ適用させた結果を30という数値と足しています。かなり不自然な関数ですが1引数の関数、Int型の値を2つ引数として渡して10+20+30を実現させてみました。

addthree3

addthree3関数でも型の定義に関数型(Int -> Int)が含まれます。引数の順番が違うだけで先ほどのaddthree2と仕組みは全く同じです。

addthree3 :: Int -> (Int -> Int) -> Int
addthree3 a f = f a + 30

plus_twenty :: Int -> Int
plus_twenty b = b + 20

main = print $ addthree3 10 plus_twenty

関数の定義は以下のように定義されています。「引数→1引数関数→戻り値」と定義されていることがaddthree3関数のポイントです。 なので引数→関数の順番に引数を渡すようにしました。

addthree3 :: Int -> (Int -> Int) -> Int

addthree4,6

addthree4,6は同じなのでまとめて説明します。(Int -> Int)(Int -> Int -> Int)があるので、関数を引数として渡す必要がありそうです。しかしここでは関数を引数として渡すとエラーになります。そしてaddthree4,6関数はaddthree1と全く同じです。なぜ関数を渡すとエラーになるか?なぜaddthree1と同じか?を説明できますでしょうか。

addthree4 :: Int -> Int -> (Int -> Int)
addthree6 :: Int -> (Int -> Int -> Int)
-- 関数を渡すとエラーになる
-- addthree1 :: Int -> Int -> Int -> Int と実は同じ

なぜなら1番最後のIntは関数それ自体の戻り値となるからです。最後に記述された(Int -> Int -> Int)は引数として受け取る関数ではなく、関数それ自体への戻り値となります(1番最後にあるデータ型は関数それ自体の戻り値となるから)。そしてそれより前にあるモノは全て引数となります。

addthree4 :: Int -> Int -> (Int -> Int)
addthree4 a b c = a + b + c

main = print $ addthree4 10 20 30
-- 実行結果「60」
addthree6 :: Int -> (Int -> Int -> Int)
addthree6 a b c = a + b + c

main = print $ addthree6 10 20 30
-- 実行結果「30」

関数型(型 -> 型)がある場所によって意味が異なりますので注意して下さい。

addthree5

addthree5は少し特殊です。引数として関数を受け取ることしかできません。なぜなら1番最後のIntは既に関数それ自体の戻り値の型として埋まっているからです。

addthree5 :: (Int -> Int -> Int) -> Int

(Int -> Int -> Int)とは2つの引数を受け取る関数です。2つの引数を受け取る関数に何らかの処理をして、Int型の値を返却する関数となっています。2引数の関数ですが無理矢理10+20+30を実現させてみます。

addthree5 :: (Int -> Int -> Int) -> Int
addthree5 f = f 20 30

addtwo :: Int -> Int -> Int
addtwo a b = 10 + a + b

main = print $ addthree5 addtwo

addtwoという2つの引数を受け取る関数を下で定義しました。addtwoは受け取った2つの引数を10と足し算します。そしてaddtwo(2引数関数)をaddthree5へ渡すことで引数を埋めました。

Discussion