🍆

高階関数,カリー化,部分適用とは?(Haskell)

2022/05/19に公開

この記事では高階関数にまつわる用語の意味を確認します。高階関数,カリー化,部分適用についてです。ゴッチャになりがちな3つの意味を整理したいと思います。

用語 意味
高階関数 引数/戻り値として扱える関数のこと
カリー化 複数の引数を1引数化すること
部分適用 引数を一度に全て渡さず、一部だけ渡す

高階関数とは?

引数/戻り値として扱える関数のことです。 従来の命令型言語(Java、C++など)では、引数/戻り値に関数は指定できません。基本のデータ型(Int、Charなど)・構造体・クラスなどしか引数/戻り値に使えませんでした。一方Haskellには関数型というデータ型があり、これによって関数を引数/戻り値として扱うことができます。

関数型とは?

関数型は以下のように定義されています。プレリュード関数even+の型を調べてみました。Haskellでは+などの演算子も関数として定義されています。

Prelude> :t not
not :: Bool -> Bool
Prelude> :t (+)
(+) :: Num a => a -> a -> a

矢印記号->が出てきました。関数の型は何かの型->何かの型という形で表されます。『ある型の引数を別の型の結果へ変換する』ということを意味します。通常のデータ型のように「扱うデータを指定する」だけでなく、「ある型から別の型へ変換する型」という機能があります。

「Haskellの関数は引数/戻り値として使える」という特徴がまず大前提としてあります。ここからカリー化という機能を実現することができます。

カリー化

複数の引数を1引数化することです。 「複数の引数を取る関数」というのはなく、「関数を返す関数」として表されます。内部的には常に1引数の関数と同じように処理されているのです。

同時に処理した場合でも、1引数化して処理した場合でも、結果的に値は同じです。しかし内部での処理プロセスをシンプルにしたり、後述する部分適用をカンタンにできるメリットがあります。

引数が2つの場合

複数の引数→1引数化するとはどういうことでしょうか。受け取った2つの引数を足し算するaddtwo関数を例に考えてみます。10、20という引数を2つ受け取っているのですが、関数は1つの引数しか受け取れないルールとなっています。この場合どのように処理されるのでしょうか。

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

10、20という2つの引数に対しaddtwo関数は以下のように処理を行います。(f(x)という関数が実際に返却されるワケではなくてイメージするための具体例です。)

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

関数の呼び出しは左結合です。addtwo関数を呼び出す場合、addtwo 10 20(addtwo 10) 20と同じです。addtwo 10は続く引数に対して20を加える関数として成立しています。そして20を引数として(add 10)を呼び出しており、結果として20という値が得られます。

Haskellの関数は常に1つの引数しか取れません。引数を複数取るには、それ用にもう1つ新しく関数が作られる点に注目して下さい。

引数が3つの場合

引数が3つある場合も同じです。引数を3つ足し算するaddthree関数はどうでしょうか。

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

こちらも先ほどと流れは同じです。引数は1つずつ関数へ適用されていきます。

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

同じく関数は左結合なので、引数:10から順番に適用されていきます。((addtwo 10) 20) 30を呼び出しするのと全く同じです。

関数が返却されることを確認

数値を返却する代わりに、新たに別の関数を返却する..と説明してきました。では本当に関数が返却されるかどうか確かめてみます。あえて引数を1つだけ渡してみるとどうでしょうか。以下のコードを実行すると次のようなエラーが出ます。

addtwo :: Int -> Int -> Int
addtwo a b = a + b
    
main = print $ addtwo 10
エラー内容
No instance for (Show (Int -> Int)) arising from a use of ‘print’

No instance = データ型がありません と出ます。Int -> Intの関数を生成したけど、それをShow型クラスでは扱えない..という内容です。引数を1つだけ渡した時点で返却されるのは関数なので、それをprint関数で文字列にすることができないのです。

ここで注目してもらいたいのが「引数が足りないよ!」というエラーではない点です。あくまで「文字列として表現できないよ!」というエラーであって、引数が1つでも問題なく処理ができるのです。

部分適用

一引数化すると何が嬉しいのでしょうか。それは部分適用ができることです。部分適用とは引数を一度に全て渡さず、一部だけ渡すことができる仕組みです。 複数あるうちの一部だけを渡すことで柔軟にプログラムを作れます。先ほどのaddtwo関数で部分適用の仕組みを使ってみます。

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

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

本来より少ない引数でaddtwo関数を呼び出しました。tasizan2には何が格納されるかといえば、tasizanからもらう関数が格納されます。そして引数としてもう1つ渡してあげれば計算させることができます。「少ない引数で関数を呼び出し新たな関数を作る」というこんなことが部分適用でできてしまいます。

部分適用の例をもう1つ見ます。数値の大きい方を返却するmax関数です。数値の100と比較をしたかったとします。この時に引数として100だけを渡して、新たな関数を作ることが可能です。

hikaku_100 = max 100
main = print $ hikaku_100 200
-- 実行結果「200」

このように部分適用をすれば、複数ある引数を全て渡さなくても良くなります。関数同士の組み合わせやすさや再利用性を高めるのに活用できます。

用語 意味
高階関数 引数/戻り値として扱える関数のこと
カリー化 複数の引数を1引数化すること
部分適用 引数を一度に全て渡さず、一部だけ渡す

それぞれの違いを理解しておきましょう。

Discussion