ニコニコ大百科モバイル

7/2(月)よりスマホまたはPCでアクセスした場合、各デバイス向けのサイトへ自動で転送致します


関数型プログラミング


ヨミ: カンスウガタプログラミング
掲示板をミル!
14カキコ!
ほんわかレス推奨 ほんわかレス推奨です!
この掲示板では、しばしばしい論争が起きそうな気がします。
コメントを書くときはスレをよく読みほんわかレスに努めてください。

関数型プログラミングとは、プログラミングスタイルもしくはプログラミングパラダイムである。参照透過プログラミングのことかもしれない。


目次



概要


関数型プログラミングとは、関数を数値などと同じように関数に受け渡しすることによって「上手に」プログラミングを行うスタイルである。文字だけを見れば関数型言語を使用してプログラミングすることともいえそうだが、的は「状態」に依存することにより生じるプログラミングミス参照透過性で防止することにあり、関数型言語の使用や関数引数にした高階関数活用などは参照透過性を容易に実現するための手段もしくは副次的な結果に過ぎない(とまで言うとやや言い過ぎになる)。また、最近はマルチパラダイムを採用するプログラミング言語も増えているため、関数型言語に分類されない言でも関数型プログラミングを行うことが可になってきている。

そのため、プログラミング言語プログラミングスタイルもしくはプログラミングパラダイムは分けて考えたほうがいいのではないかということで、関数型言語以外に関数型プログラミングという用を使用することがある。


歴史


黎明期

大きく注されるようになったのは2010年代に入ってからではないかと思われるが、その歴史は非常に古く、世界で2番に古いプログラミング言語とされるLispにまでさかのぼる。しかしながらLispの構造は当時のコンピューターで実行するには重すぎたため、そこそこの可読性を備えつつ高いパフォーマンスを発揮するC言語などに押されて忘れ去られてしまった。

再登場

その後しばらくは表舞台からは姿を消してひっそっりと受け継がれてきたが、やっと時代が追いついてきた

その要因として一つは作成するプログラムの高機化に伴う開発の複雑化が挙げられる。開発の現場で人為ミスが多発し、視できないほど生産性が損なわれるようになってきた。従って人為ミスを減らす環境に対する需要が開発言を変更することまで視野に入ってくるほど高まってきたのである。

二つコンピューターの向上である。かつて何十万円出しても買えなかったようなコンピューターが、今やその数倍以上の性のものが数万円で手に入るようになった。これによりプログラムを実行するPCの負荷が増えても、プログラムを開発する人間の負担が減った方がよいと考える土壌が形成されるようになってきたのである。

三つ目にはCPUマルチコア化があるといわれている。マルチコアの性を引き出すには並列処理が欠かせないが、後述のように関数型プログラミングの参照透過性と並列処理は相性がいい。

最近の動向

関数型プログラミングができる言としては最古参Lisp以外に、純関数型言語HaskellJava仮想マシンで動作するScala,ClojureMicrosoftOCamlからお得意のパクりで作ったF#Appleこれもお芸の旧環境切り捨てをして打ち出したSwiftなどが続々と参入してきている。実はJavascriptも初期から関数型プログラミングが可であった(元はブラウザLispしようとしたのが開発動機だったとか)。さらにはオブジェクト指向の代表格ともされるJavaですら2014年からは関数型プログラミングを言仕様に取り入れるようになった。


オブジェクト指向との論争


前述のようにオブジェクト指向よりも歴史が古いともいえるわけだが、注されるようになったのはオブジェクト指向よりも後であるため、オブジェクト指向の次に来るプログラミングパラダイムとみる人が多い。

それ故に関数型プログラミングに心酔する人の中にはオブジェクト指向批判・否定する人も少なくなく、オブジェクト指向を使っている人との間に宗教戦争最強議論に近い感情的対立がある人もいるようである。

共存できないのは人間であって、プログラミングパラダイム自体はオブジェクト指向と関数型プログラミングが共存可であるとみる人も多い。チューリング完全である以上、できるできないの差ではなく、やりやすいかやりにくいかだけの違いなのではなかろうか。

[目次へ]


特徴


何をもって関数型プログラミングとするかが問題なのだが、歴史が古いと言いながらも、注されてからの歴史はまだまだ浅い。そのため細かい点ではコンセンサスがなく言った者勝ちみたいなところ(この記事にもあてはまる)がなくもない。

以下に、だったものを列挙する。


高階関数


関数引数にしたり戻り値にしたりできる関数高階関数という。これが使えることが関数型プログラミングを行うための最低条件であることは、おそらく共通認識とみて問題ない。

実際に関数プログラミングが可な言高階関数として実装される定番には以下のものがある。それ以外のものは言仕様上需要がある場合を除きあまり取り上げられない。

map(写像)

関数リスト引数にとりリストの各要素に関数を適用した戻り値を要素とするリストを返す。

filter(フィルター)

偽値を返す関数リスト引数にとり関数を適用するとになる要素のみからなるリストを返す。

fold, inject, reduce(重畳関数)

初期値と、リストと、「引数を2つとる関数」を引数に取る。言により関数名の違いが大きい。

  1. 初期値とリストの1番の要素に関数を適用する。
  2. 上記の戻り値とリストの2番の要素に関数を適用する。
  3. 上記の戻り値とリストの3番の要素に関数を適用する。
  4. これをリストの最後の要素まで繰り返し、最後に関数を適用した結果を戻り値として返す。

畳み込みと呼ぶこともあるが、日本語では畳み込みは数学の違う計算[外部]すことがあるもよう。

クロージャ

関数の中に、関数の外部の値への参照が閉じ込められている関数のこと。外部の値が変わるとそれに合わせて適切な挙動ができるということがメリットとして挙げられることがあるが、数学関数の定義からは外れているようにも思える

カリー化

複数の引数をとる関数を、引数を一つずつとっていく関数にすること。Haskellでは関数は標準でカリー化されているので言仕様上重要だが、それ以外の言ではあまりおにかからない

[目次へ]


関数(数学)


プログラミング関数といえば、計算する以外にも入出をこなしたりと様々な機を持っているFunctionす。しかし関数型プログラミングにおける関数は、数学的な関数をいう。数学的な関数は計算のみを行い周囲に何の変化ももたらさない。また、数学関数においては、環境によらず同じ引数からは常に同じ戻り値が得られる。


参照透過


イミュータブルという表現も用いられる。プログラム中で変数は常に同じもの(値)をしているということ。プログラム内のどこからでも、値を参照すると初期設定値が透けて見える(途中で値が変更されていると、初期値を知ることはできない)という意味。

変数の値が変わる環境では、プログラムの文面上ではその変数がどういう状態にあるか(たとえばその変数に初期化処理がされているかどうか)がわからず、それが人為的ミスの元であるという考え方があるのではないかと思われる。


副作用禁止(再代入不可)


参照透過ということは、ものごとの状態は変更されず、値は一度代入されてしまったら、再度代入して変更することは出来ないということである(それゆえ代入という表現を嫌い、束縛という用を用いる言もある)。

このことを突き詰めると結果の画面表示ですら状態変更に該当し認められないことになってしまうため、一部の批判的な人は、この点を「外に出ようとしないひきこもり」にたとえたりすることがあるが、実際の関数型プログラミングは何らかの方法で妥協している。ただしその際でも、副作用のある部分と参照透過性のある部分の明示的分離が推奨されている。


モジュール化・抽象化


「状態」があると「状態」がいつ変更されるのかを常に意識しなければならないが、参照透過性により「状態」に依存したプログラミングから脱却できる。依存がなくなるということは、関数と計算対独立性が高くなるということであり、プログラムモジュール化(独立した部品化)が促進されるらしい

さらに計算対からの独立性が高くなると、計算対の種類によらないプログラムフロー(繰り返しなど)を高階関数で表せるようになり、高度な抽化を実現することができるようになるようだ

抽象化の問題点

プログラミングパターン高階関数として抽化して簡潔なプログラムを書くことができる反面、プログラミングパターンの数だけ高階関数が存在することになるので、パターンの組み合わせも考えると、各種高階関数を学習するコストが幾何級数的に増加するおそれがある。

たとえば、上で述べた重畳関数は(i = i + 1を使うタイプの) for ループ高階関数 fold の適用に置き換えることができるが、リストの後ろから順番に計算したくなった場合、for ループであれば初期値をリストの先頭から末尾に変更し、 i = i + 1 を i = i - 1 にするだけであり、そこに新たな知識や定義は必要ないのに対し、高階関数fold は後ろ(右)から順に計算する高階関数foldr を別途定義するか、リストを逆順にするreverseのような関数を別途定義しなければならない。


ポインタ問題からの解放


C言語の時代からポインタがわかりにくいと言われている理由は多数あるが、その理由の一つに「ポインタと実体の乖離」がある。同じメモリ間を参照する別のポインタからの操作により、気づかないうちにポインタの参照先が別の内容になっているということが起こりうるのである。

それ以上に多いのが、効なポインタを有効な値に変更し忘れるという、いわゆる「ぬるぽ」にまつわる問題である。

参照透過性が保されていればこれらの問題は解決する。一度書き込まれた内容は変更されることがないのだから、参照先が別の内容になっているとか、後で有効な値に変更しなければならないということ自体が起こり得ないからである。


遅延評価


実際にその値が必要になるまで計算しないで放置しておくこと。反対正格評価。f(g) = h (f, g, hは関数)とした場合、h(x)は計算できても h そのものについては計算のしようがないことがほとんどなので、ある意味仕方のない場合もある。

これが許される背景としては上記の参照透過性が欠かせない。後で計算しようとした時に引数の値が別の処理で変更されていたら結果もまた別のものになるからである。

計算しようとすると条件によってはエラーが発生する値も、エラーが発生する状況では必要とされない場合、計算式に含めることができる。この性質を用いてリスト(要素数限個のリスト)を(容易に)定義できるのが特長。

必要のない計算をしないのでパフォーマンスが向上するという話もあるが、インタラティブなアプリケーションの場合はいた時間に計算してしまったほうがよいような気がしないでもない

[目次へ]


並列処理


並列処理プログラミングの最大の問題として、複数のスレッドが同時に同じ変数に値を書き込むと誤動作するということがある。また、これを回避するためにスレッド変数アクセスする順番待ちの制度が用意されることがあるが、順番を次にまわさないスレッドがでるとプログラムフリーズする。

前述のように、関数型プログラミングではイミュータブルな値しか使わないので複数のスレッドが同時に値を書き込むことがない、というか値を書き込むのは初期化する一回限りである。したがって、上記のような誤動作やフリーズ(デッドロック)の心配をせずにマルチコアCPU向けに処理を並列化できる。

というのが建前だが、実際には副作用についてなんらかの妥協が行われているので、並列化する部分にその妥協が紛れ込んでいた場合には当てはまらないかもしれない。また、並列化された処理同士がお互いの処理結果に依存しないようアルゴリズムを設計しないといけないという点は、関数型プログラミングでも変わらない。


ループ処理(再帰)


関数型プログラミングのスタイルとしてよく例に挙げられるのが、forループの代わりに再帰を用いるということがある。なぜこのようなまどろっこしいことをするかというと、的なforループでは i = i + 1 のような処理が含まれるので、これが再代入禁止の原則に反するというのがな動機である。なお、i = i + 1 が入るようなループについては連続する整数からなるリストに上述のmapやfoldなどの高階関数を適用することでも実装できる。

forループが「同様な処理」を「ある状態において(for)繰り返す」という、処理と条件判断・繰り返しがはっきり分かれた構造をしているのに対し、再帰によるループは「ある処理を行う(ただし、ある処理には繰り返し条件を判断して次の同様な処理を呼ぶことが含まれる)」というように、的の処理と条件判断・繰り返し部分がつながった構造になっている。自然と同じで慣れの問題かもしれないが、違和感を感じる人がいるとしたら、for文ではループの存在が for, while などのキーワードで明示されているのに対し、再帰では書式自体は通常の関数呼び出し同じであるといった構造的な違いが原因かもしれない。

誤解のないように述べておくと、再帰自体はループ専用の概念というわけではなく、2分木構造の実現など様々な使用法がある。

末尾再帰最適化問題


次へ»
最終更新日: 17/05/13 22:45
タグ検索 パソコン版を見る


[0]TOP
ニコニコ動画モバイル
運営元:ドワンゴ