近況

自宅サーバーが起動しなくなったため、中身に保管してある電子書籍のPDFが取り出せず、大量の知性が失われている。

要旨

以前に「関数型プログラミングの初心者」に向けて質問したときに、そもそも関数とはなにかについてわからなかったという質問があった。自分も、具体的に関数とはなにか、というと説明に困ることがある。そこで、今回のブログでは「関数」とは何かについて、自分なりにまとめたことを書く。

はじめに

知り合いの技術者は、「実は関数の考え方について、一年間くらい馴染めなかった」と言っていた。現在では、開発をバリバリやっているような人であり、自分も尊敬しているのだけれど、そういう人でも「関数」という考え方について、実はそれほど馴染めなかったということを聞いてビックリしたりしていた。

とはいえ、そういう風に「そうなのか」と納得している俺も、実際のところ、では「関数」とは何か、というのを説明できるくらいには理解しているとも言えないし、また他の人もそもそも関数とはなにかについてわからなかったというくらいだ。なので、正確な説明になっているかどうかについては自信は無いけれど、「関数」についてどういうものかを、自分なりにまとめてみるのが、今回のブログの主旨となる。

プログラムにおける三種類の関数

あまり正確ではないとは思うものの、乱暴にプログラムにおける三種類の関数について、まとめるとするならば、次のようになるだろうと思う。

手続きとしての関数(procedure) 数学的な関数を、できるだけプログラミング的に表現するための関数 数学的な意味での関数(function)

もちろん、1と2は明確に分けられるものではない。というのは、如何なるプログラミング言語であっても、IOであったり、データベースとのやり取りなど、その部分において、何らかの形で手続きをする必要がある。しかし、その手続きを、「手続きの必要な部分」と分離し、出来るだけ「数学的な表現」に近づけることは可能だ。ただ、果たして数学的な関数と、プログラミング的な関数が一緒かどうかは一考する必要がある。

例えば、SICPにおいては「関数と手続きの対照は, もののあり様の記述と, ことのなし方の記述の違いの反映である. あるいは, よくいわれるように, 平叙文的知識と命令文的知識の違いである. われわれは数学では通常平叙文的(何である)記述に関心を持つが, 一方, 計算機科学では命令文的(どうする)記述に関心を持つ」としており、このような「どうする」の集積が「手続き」と言うことが出来るが、註釈にもある通り、「何である」から「どうする」を記述するという方法は、進展のある領域だと見て良い(その一つの成果がHaskellとOCamlだと言える)。

とはいえ、「数学的な関数を、できるだけプログラミング的に表現するための関数」としたときに、そもそもの「数学的な意味での関数」という、その内実を述べなければならないだろう。

数学的な意味での関数について

数学的な意味で関数を述べる場合、次のような定義となる。以下は『数学のロジックと集合論』(p.47)より引用する:

を から への関係とする。各 に対して、 となる がただ一つ存在するとき、 を から への関数とする。

このとき、 とは何か、という問題にぶち当たるのだが、個人的にわかりやすかった例としては、次のような図の説明となる:

これを、いわゆるよく見る集合図の「元(つまり要素)」として考えることが出来る:

このとき、 を定義域と呼び、 を値域(終域と書いている本もある)としている。用語をちゃんと知るのも重要だが、関数において重要になるのは、噛み砕くとするならば、次のような言葉になるだろう(『現代数学の考え方』(p.100)より):

関数は、ある集合の上で定義される。

それは、ある集合の中の値を取る。

与えられた要素に対する関数の値をただ一通りに定めるような規則によって関数がきまる。

と呼ぶことが出来る。これは、プログラムにおける関数においても、直感的にわかるだろう。例えば具体的に という、奇数を出す関数を考える場合:







...



といった奇数のリストが取り出せるのだが、ポイントは の に対して値を入れると、ある値が出てくるということになるだろう。

寄り道: 手続きとしての関数をHaskellで考える

しかし、プログラムにおいては、関数と述べるとき、単純に「サブルーチン」、つまり手続きの集積体として考えられるだろう。例えば「関西関数型道場」において「手続き」とは何かについて、まとめてある(28:00辺り)。

Haskellにおいては、手続きはモナドとして実現出来るため、返り値を返す。

putStr :: String -> IO () putStrLn :: String -> IO ()

Haskellにおいては、最後を返り値とし、それ以外を引数とする。つまり、 putStr と putStrLn は、 String の型を引数として取り、そして IO() を返す関数であるということである。そして、Haskellの場合において、 main 関数を実行するときに、 IO() を返り値として返すように設定されている。

main :: IO() main = do putStr "Hello, " putStr "world." putStrLn ""

このように、手続きを書くことが出来るのだが、これは次のように書くことも出来る。

main :: IO() main = putStr "Hello," >> putStr "world." >> putStrLn ""

ここで、 >> というオペレーターは何なのかという話なのだが、 >> の定義を見てみると、次のようになっている。

( >> ) :: Monad m => m a -> m b -> m b

ここで、 >> とは、引数に二つの IO() という「アクション」の値を返す関数を実行し、また次にふたつ目の「アクション」の値を返す関数を実行し、結果としては「二つ目のアクション」の値を返すというものであるということがわかる。Haskellに詳しくないので間違っているかもしれないが、ポイントになるのは、 putStr で必要なのは、画面に出力という「手続き」が欲しいのであって、その結果が欲しいわけではないし、それを引き受ける相手も存在しない。なので、この場合は最初の IO () という「返り値」を無視をしており(手続きはちゃんと実行される)、二番目の IO () の返り値を戻す、という風に考えることが出来ると思う。これらは複数人の指摘によって修正された。指摘してくれた方、ありがとうございます。

手続きとしての関数

このように考えると、文字を出力するといった場合に、出力するという過程が欲しいのであって、その結果が欲しいわけではない、という風に考えることが出来る。例えば、JavaScriptなら、文字出力は次のように書ける。

function hello() { console.log( 'Hello, world.' ); } hello();

あるいはC言語なら次のように書ける。

#include <stdio.h> void println( char print_string[]) { printf( " %s

" , print_string); } void main( int argc, char *args[]) { println( "Hello, world." ); }

このとき、 JavaScript に顕著なのだが、特に関数として何らかの値を返却しなくても良いようになっている。あるいは、CではLinuxに対して、正常に終了したことを伝えるために、 return 0; をするほうが行儀がいいのかもしれないが、このように void でも正常に動く。

このように考えたとき、基本的に「手続きとしての関数」は、その「過程」が重要なのだが、その結果が必ずしも必要なわけではない。そうすると、最初に与えた「数学的な意味での関数」とズレが生じてくる。このようなサブルーチンとして、便宜上名前が付いている「関数」と、いわば「数学的な意味に近い関数」というのは分けたほうが、混乱が少なくて済むように、個人的には思う。

まとめ

もちろん、これがプログラムにおける「関数」の全容ではないが、「関数」についての一部の考え方については、まとめられたつもりである。全容ではない、と断ったのは、実際には、この「関数」について、値として扱い、その結果として関数を扱う関数、「高階関数」という考え方が出来るからだ。これについては、また後日ブログの記事にしようと思う。

参考文献

関数については、この本の解説が一番わかりやすかった。

補助的な本として。

Haskellの理解を深めるために。

あわせて読みたい

Haskell部分に関しては、はるかにこっちのほうが正確な記述だと思われます。