関数型プログラミング単語

14件
カンスウガタプログラミング
8.7千文字の記事
  • 2
  • 0pt
掲示板へ
ほんわかレス推奨 ほんわかレス推奨です!
この掲示板では、しばしばしい論争が起きそうな気がします。
コメントを書くときはスレをよく読みほんわかレスに努めてください。

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

目次

概要

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

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

歴史

黎明期

大きく注されるようになったのは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. これをリストの最後の要素まで繰り返し、最後に関数を適用した結果を戻り値として返す。

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

クロージャ

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

カリー化

複数の引数をとる関数を、引数を一つずつとっていく関数にすること。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分木構造の実現など様々な使用法がある。

末尾再帰最適化問題

再帰の中でもforループの代わりの再帰では、ループの末尾に再帰呼出しがくる。再帰呼出しは関数の呼び出しだが、関数の実行が終わったら戻ってくる。現実の問題としてたいていの処理系は、関数を呼び出すと戻ってくる場所に関する情報メモリに保存している。

繰り返し数が膨大になると、この戻ってくるための情報メモリ圧迫オーバーフローを引き起こす。

関数型言語の一部では、この問題を避けるために末尾再帰に限り、呼び出しで戻る場所を最後に戻ってくるポイントだけ記憶するという最適化コンパイラや実行系によって行われるようになっている。これを末尾再帰最適化(Tail Call Optimization)という。

末尾再帰最適化がない言でもトリッキーな方法でオーバーフローを回避することが可らしいが、そもそも末尾再帰最適化がない言では for ループが使えるはずなので、理に再帰を用いることはないのかもしれない。

無名再帰問題

再帰を使うには処理内容を記述した関数再帰呼び出しするために、その処理内容を記述した関数自身を名前が必要になる。しかし、非関数型プログラミングでforループを使用する頻度を考えれば想像がつくように、繰り返し処理のたびに処理に名前を付けていたのではすぐに名前空間での衝突が起こり、付ける名前がなくなってしまう。

これを回避するには関数(ラムダ式のように、関数名前を付けずに関数の処理内容のみを記述する関数定義方式)の中から(予約などで)その関数自身を呼ぶことができればよいのだが、これができるかどうかは言仕様に強く依存する。

しかしながら、この機がない言ではforループ(ry

[目次へ]

定理証明

プログラムに「絶対に」バグがないことを確認するのは通常であれば不可能に近い。しかし数学関数であれば、数学の定理として明してしまえば、バグがないことが保されるのである。人為的ミスの削減の究極の形といえ、関数型プログラミングなら可になるのではといわれている。実現していないこともないが、一般的な用途での実用には程遠い段階である。

分岐

おや、誰か来たようだ

宣言型プログラミング, FRP(Functional Reactive Programming)

関数型プログラミングでは参照透過性が原則になるため、GUIなど時々刻々と変化するものは相対的に扱いづらくなっている。そこで出てきたのがFunctional Reactive Programmingなどに代表される宣言型プログラミングという考え方である。

宣言型プログラミングでは、変化する物事の中でも変化しないもの、すなわち物事の間の関係性に着した。たとえば b = a / 2 という関係を宣言しておけば、 a が変化すれば、自動的に b が更新されるといった具合である。GUIでいうならば、ある欄に記入された数値の座標に点が表示されると宣言すれば、数値が変更されるたびに点が移動するというような状態である。

副作用を扱うための制約が大きい関数型プログラミングで大きなご利益があるといえるが、関数型プログラミングでない言でも実装されており、関数型プログラミングの専売特許というわけではない。

まとめ

  1. 参照透過性により変数の状態に依存した人為ミスや並列処理に伴う人為ミスが減る。遅延評価が可になるというおまけもついてくる。
  2. 高階関数を用いると便利なことがある。
  3. 関数の言プログラミングスタイルは、数学関数を通じて参照透過性の利用と副作用の分離を促す(強制までされることはまれ)ことにより、人為ミスを減らし、生産性を高めようとしている。
  4. 大事なのは言プログラミングスタイルよりも、人為ミスを減らせるかどうかなのではなかろうか?

異論は認める

[目次へ]

考察

関数型プログラミングの良さが説明しにくい理由

関数型プログラミングのメリットを人為的ミスの減少にめるなら、よくみかける「関数型言語ではこういうことができます」という成功体験論よりも、「他のスタイルでやってしまうミスが、関数型プログラミングでは注意しなくても起こりません」のようなネガキャン失敗談的教訓の方が説明としてはわかりやすいのかもしれないが、失敗のパターン数にあり得るため、そういった観点からの体系だった説明はあまり見かけない

また、人為ミスというのは単純な例では起こりにくい。従って把握するのが困難なほど複雑な事例を示さないと関数型プログラミングの良さを示すことはできないのかもしれない。しかし把握するのが困難な事例というのは、説明に用いるのに適さないという自己矛盾を抱えているため、こういった観点からの説明もみかけることはない。

異論は認める

関数型プログラミングという概念がわかりにくい理由

まとめで述べたように、関数型プログラミングのメリットというのは単一のものではなく、複数のメリットからなる。しかもその中で大きな割合を占めているのは参照透過性によるものであり、関数型プログラミングという名称から連想される高階関数によるメリットは2番手に過ぎない。定理証明? なにそれおいしいの?

また、それぞれのメリット自体も性質が異なったものである。参照透過性のメリットは「ミス防止」という「マイナスゼロにする」「だれにでもあてはまる」性質であるのに対し、高階関数は「理解に応じて高度で抽的な操作ができるようになる」という「ゼロプラスにする」「使い手を選ぶ」性質である。前述の表現を踏襲するなら定理証明は「プラス(ユニットテストによる検証)を無限大(完璧)にする」性質とも言えるが、「実用上は使える人がいない」のでそこまで考える必要はない。

さらに、関数型プログラミング・参照透過性・高階関数というものは一対一に対応するものではない。まず、参照透過性は高階関数がなくても実装であり、高階関数的というよりは参照透過性を保ったまま便利にプログラミングするための手段と見ることもできる。そして、参照透過性は関数型プログラミングの重要な要素であるが、非純関数型言語の例をみればわかるように、関数引数にとる高階関数には副作用のない関数を渡すという関数型プログラミングの建前に反して、副作用のある関数を渡せるよう言仕様を定めることは可である。このように関数型プログラミング・参照透過性・高階関数というのはお互いに重なりあいつつも、どちらかが一方的に他方に含まれるという関係でもないのである。

重なりあうところがありつつも性質が異なる複数の概念を一つの名前で呼んでいる上に、名前通りに考えた意味とは違う意味が第一義に来るため、人によってしているものが異なり混乱があるのかもしれない。

関数型プログラミング

参照透過プログラミング

高階関数

  • 高度な抽

もちろん異論は認める

[目次へ]

関連商品

関数型プログラミングに関するニコニコ市場の商品を紹介してください。

関連項目

[目次へ]

【スポンサーリンク】

  • 2
  • 0pt
記事編集 編集履歴を閲覧

この記事の掲示板に最近描かれたお絵カキコ

お絵カキコがありません

この記事の掲示板に最近投稿されたピコカキコ

ピコカキコがありません

関数型プログラミング

7 ななしのよっしん
2015/11/22(日) 10:56:33 ID: h6Z270BeLs
>>5
>>2でお答えしたとおり、この話題についてのよそでの議論を見てからの結果ですのでさしあたって変更するつもりはありません。あしからずご了承ください。
> ギョッとしましたね
それは仕様です。記事を読むとき、掲示板に書き込むときは、荒れている記事を扱うときの注意と警をもって接していただければと存じます。
👍
高評価
0
👎
低評価
1
8 ななしのよっしん
2017/06/18(日) 01:14:30 ID: vxde946Efh
関数型プログラミング閥で争ってるとか想像しながら掲示板見たらほんわかしてて
👍
高評価
0
👎
低評価
0
9 ななしのよっしん
2019/05/31(金) 20:27:31 ID: OYr5jvVZ+w
日本における関数の第一人者毛の壁さんについても少しは触れてあげて
👍
高評価
0
👎
低評価
0
10 ななしのよっしん
2019/07/16(火) 17:38:55 ID: WL5n1g0ghe
>>7
うーん。も正直「ほんわかレス推奨です」は不要だと思う。
というかこれが原因で荒れることも十分考えられる訳だし。ほんわかレスを推奨してても荒れる時は荒れるので、総合的なリスクを勘案すると、今は除去したほうがいいと思う。

多数決とか採る?
👍
高評価
0
👎
低評価
0
11 ななしのよっしん
2019/07/16(火) 21:40:09 ID: h6Z270BeLs
>>10
関数型プログラミングに関する議論に造詣の深い方か、自治の精に燃えていらっしゃる方かで、こちらとしては違う受け止め方をしますが、後者の方でしたらお引き取り願います。
👍
高評価
0
👎
低評価
1
12 ななしのよっしん
2019/07/22(月) 21:43:37 ID: WL5n1g0ghe
えぇ…ただの意見表明なんだけどこれが「自治の精に燃えている」扱いになるんですかね?
👍
高評価
1
👎
低評価
0
13 ななしのよっしん
2019/09/26(木) 09:46:06 ID: OYr5jvVZ+w
十分なるでしょ
それも今までネットに存在してきた中でも最高レベルのに当てはまる
👍
高評価
0
👎
低評価
1
14 ななしのよっしん
2019/11/07(木) 12:10:02 ID: WL5n1g0ghe
👍
高評価
0
👎
低評価
1
15 ななしのよっしん
2021/05/01(土) 23:52:55 ID: Ec4m+I8/HP
ほしかった情報が結構書いてあり助かった。
取り消し線はギョッとしたけど読了したら納得感あった。

経験則で極イミュータブルにしておくとヒューマンエラー減らせる、というのがあるんで、
その観点から関数型プログラミングのいろんな概念
取っ掛かりなりともつまみ食い読みできたのはありがたい。
👍
高評価
1
👎
低評価
0
16 ななしのよっしん
2021/05/03(月) 12:01:46 ID: h6Z270BeLs
>>15
ありがとうございます
構想・下書きの段階からかなり手間をかけて作った甲斐がありました。
お褒め頂けて嬉しいです。
👍
高評価
1
👎
低評価
0