UP
混沌のプログラミングパラダイム
前のページへ
プログラミング
次のページへ
レキシカルスコープとレキシカル変数とクロージャと


進撃の関数型プログラミング

【 目次 】

関数型プログラミングなるものがちまたでははやっている。

JavaやC#でも関数型プログラミングのための機能が取り入れらるようになってきている。
.NET FrameworkにはF#,JavaVMにはScalaという関数型言語がある。

  • 記者の眼 - Java 8は関数型なのか:ITpro

    関数型プログラミングに由来する「ラムダ(lambda)式」なる構文が導入...
    「Stream API」というコレクション操作APIが導入され、データの集合であるコレクションから特定の条件に合致した要素のみを抽出するといった処理を簡便に記述できるようになる。...
    関数自体を他のデータ型などと同様に変数に代入したり、関数の引数に使ったり返り値に使うことができる、いわゆる「第一級関数(ファーストクラス関数)」の仕組み...
    変数への代入は一度のみとし、書き換えは行わない「イミュータブル(immutable)なデータ構造」...
    オラクルの公式見解は「マルチコアにおける並列処理を導入しやすくするため」...
    かなり明確に「関数型のプログラミングスタイルへ向かいたい」と述べている

関数型プログラミングって、いったいどんなものなのかな?

無知な愚鈍人がわからないなりにいろいろ調べてみた結果のいいかげんなレポートである。
まっ!いろいろと間違いがあると思うが愚鈍な愚鈍人の事、許してやって下さい。

これを読めばきっと君もますますわからなくなる関数型プログラミング。

と、 まずは言い訳から。

今回も、私には理解できない点が多いので、参考になりそうな他のサイトの記事からいろいろと引用しながら進めていく。

関数型言語とは

う~ん!、何だかよくわからない。
データ型の型推論というのは何となくわかるが、高階関数遅延評価っていったい何だ!

  • 関数型言語 - Wikipedia

    何をもって関数型プログラミングとするか、関数型プログラミングを行っているコミュニティ内でも正確な定義や合意というものは存在しないが、一般的には、手続き型プログラミングがコマンド実行の列としてプログラムを記述していくのに対し、関数型プログラミングは複数の式を関数の適用によって組み合わせていくプログラミングスタイルである、ということは広く認められている。

関数型プログラミングに対するはっきりとした定義は存在しないのか。

「『関数型言語』を使ってはいけない」という意見も。

関数型プログラミングはプログラミングパラダイムなんだだそう。

関数型プログラミングをめぐる言葉

下記の記事が参考になったのでいろいろと引用させていただいた上に、更に蛇足を加えてみた。

関数型プログラミングとオブジェクト指向プログラミングは概念として直交しており(are orthogonal)、共存は十分可能で、双方の利点を一度に得ることもできる

ふむ、関数型でありかつオブジェクト指向でもあるプログラミングも可能なんだ。

LISP と ISWIM はどちらも「ラムダ計算」という計算モデルを元にしているので、「関数型プログラミングは歴史的経緯上ラムダ計算を参考に作られたプログラミングパラダイムである」という言い方はできるかもしれません。

う~ん、ISWIMって?

  • ISWIM - Wikipedia

    SWIM は、...抽象プログラミング言語(あるいはプログラミング言語ファミリ)である。
    実装されたことはないが、その後のプログラミング言語の開発に多大な影響を与えた。...
    、ラムダ計算の関数型コアを命令型言語の糖衣構文で包んだものである。

実在しない関数型言語っぽいプログラミング言語のモデルってことかな?
ところでラムダ計算って何?

関数型プログラミングにおいては、できるだけ小さい処理を行う関数を用意して、それらを組み合わせてプログラムを書く傾向があります。

小さい処理の組み合わせで全体的に複雑な処理をおこなうって、いままでのプログラミングと一緒ジャン。
でも、関数型プログラミングはよりその傾向が強いって事かな?

副作用参照透過性

副作用が無いという事は参照透過性があるという事。

その副作用があると何が悪いのかというと、副作用は「参照透過性」を壊してしまいます。
参照透過性というのは、「同じ条件のもとでは必ず同じ結果になる」性質を指します。
また、ある関数において参照透過性が成り立っているとき「関数が純粋である」と呼び、そうではないものを「関数が純粋ではない」と言ったりします。

逆に言うならば、「参照透過性が無い」(関数が純粋でない)という事は、
プログラムの状態によっては、同じ条件のもとでも同じ結果にならないという事で、これは「副作用がある」という事。

単純な例でいうと、多分こんな事だと思う。(JavaScriptのコード)

var global_var = 0; // グローバル変数

function func(arg_var) {
    global_var += arg_var;
    return global_var;

関数funcは関数内部でグローバル変数global_varにこれまでの引数arg_varの値を加算していくので呼び出されるたびに異なる値を返す。
このため、関数funcは同じ条件(引数)のもとでも同じ結果にならないという事で、副作用があり,これは参照透過性が無い(関数が純粋で無い)という事。

これに対し

function func2(arg_var) {
    return arg_var * 2;
}

関数func2は同じ条件(つまり引数の値が同じ)であれば、常に同じ結果を返すという事で、副作用が無いという事。

参照透過性が成り立っていると何が嬉しいかというと、...複数の機能を組み合わせる際に、ある機能のせいで他の機能に影響が出ることがありません。

そして、副作用が無いという事は同時に複数の処理を実行しても、互いに影響が出ないので並列処理が可能という事。

計算効果

ファイルなどを読み書きする入出力や、同期を取る必要のある並列処理、いつ起こるかわからない例外だけでなく、変数への再代入(同じ式なのにあの行とこの行で値が違う!)すら実現できなくなります。
これらのように、純粋な関数で扱えない操作をまとめて「計算効果」と呼びます。

ふ~ん、純粋な関数ではファイルの入出力や変数への再代入すら実現できないのか。
これでどうやってプログラミングするんだろう?

「純粋関数型プログラミング」と「非純粋関数型プログラミング」

純粋な関数のみで関数型プログラミングをするスタイルを「純粋関数型プログラミング」、非純粋な関数も使うスタイルを「非純粋関数型プログラミング」といいます。 純粋関数型プログラミングと非純粋関数型プログラミングはどちらが優れている・劣っているということはなく、両者とも広く使われています。

モナド

モナドは純粋な関数の圏と他の圏を行ったり来たりするのに使われます。

つまりモナドは、純粋な関数の世界で実現できないことを Kleisli やその他の世界に行ってきて実行してきて、また純粋な関数の世界に存在できる形にして持って帰ってくる乗り物のようなものと言えるでしょう。

関数型プログラミングの話題でなんの断りもなしにモナドとか出てきたら、多分純粋な関数でプログラムを書く話をしてる。

遅延評価

遅延評価とは、使われるまで式を評価しない方針のことです。

純粋関数型プログラミングに遅延評価は必須というわけではないらしいです。

必要になるまでは余計な事はしないという事か?


他の記事からも関数型プログラミングについて有用と思われる部分を引用。

関数型言語は静的型付き動的型付きに分けられるよう。

  • 古くて新しいプログラミングパラダイム~関数型プログラミング | NTTデータ

    システムの保守性を向上しやすい、並列実行しやすいといった利点...
    関数型プログラミングでは、「同じ入力には必ず同じ出力を返す」という性質を持つ「関数」の定義と、この「関数」の呼び出しだけでプログラミングを行います。
    「同じ入力には必ず同じ出力を返す」という性質は、簡単に言うと関数の出力は入力だけで決まり、他の要素からの影響を受けないということです


型推論

  • 型推論 - Wikipedia

    型推論(かたすいろん)とはプログラミング言語の機能の1つで、静的な型付けを持つ言語において、変数や関数の型を宣言しなくてもそれを導くのに使われた関数の型シグネチャなどから自動的に型を決定する機構のこと。
    推論に失敗するとその時点でエラーを報告できるため、少なくとも誤った型を用いる事によるバグは回避できる。また、記述をアルゴリズムに集中できるのでプログラムの抽象度が上がるというメリットもある。
    ...
    静的型付け関数型言語のほとんどが型推論の機能を持っている。

関数型言語では無い(?)C#でも型推論の機能を持っているし、型推論は関数型言語に必須というわけでは無いが関数型言語のほとんどが型推論の機能を持っているという事か。

第一級オブジェクト(ファーストクラスのオブジェクト),第一級関数,高階関数

第一級オブジェクト

  • 第一級オブジェクト - Wikipedia

    第一級オブジェクト(ファーストクラスオブジェクト、first-class object)は、あるプログラミング言語において、たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。ここで「オブジェクト」とは広く対象物・客体を意味し、必ずしもオブジェクト指向プログラミングにおけるオブジェクトを意味しない。

で、もって

第一級関数

この、「関数が第一級オブジェクトである」というやつが第一級関数を指すらしい。
つまり、生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる関数という事。

高階関数

  • 高階関数 - Wikipedia

    高階関数(こうかいかんすう、英: higher-order function)とは、関数(手続き)を引数にしたり、あるいは関数(手続き)を戻り値とするような関数のことである。 ... 高階関数は厳密には第一級関数をサポートしているプログラミング言語において定義されるため、一般にリフレクションなしではプログラムの実行時に動的に生成することができないCのような関数ポインタをサポートしている言語は第一級関数をサポートしているとは見なされていない。

高階関数は関数を引数にしたり、あるいは関数を戻り値とするものであり、引数や戻り値の関数もまた高階関数となり得る。

第一級関数であれば高階関数であるという事か

つまり、第一級オブジェクトの中で、関数が第一級オブジェクトであるものが第一級関数と言うって事か、
そんでもって、第一級関数であれば、高階関数とも言われるということか。

以下も参考に。

  • Pythonの基本的な高階関数 - Life with Python

    高階関数とは、英語の「higher-order function」の訳で、「他の関数を引数として受け取る関数」のことです。 「配列の各要素に処理をしたい」ときなんかに、処理を簡潔にわかりやすく記述することができます。 Pythonの場合は「関数はオブジェクトであり、関数に()をつけると初めて関数が呼び出される」決まりとなっています。 ()なしで関数の名前だけを指定すればその関数そのものを捉えることができます。

無名関数

  • 無名関数 - Wikipedia

    無名関数(anonymous[1] function, nameless function)とは、名前付けされずに定義された関数のことである。無名関数を表現するための方法には様々なものがあるが、近年主流なのはラムダ式による記法である。
    無名関数を表現するリテラル式は、関数リテラル(function literal)とも呼ばれる。値がある場合は関数オブジェクトであるものが多い。

無名関数とは呼び名のとおり名前の無い関数の事。
でもって、無名関数を定義するのに後述の「ラムダ式」の記法が良く使われるらしい。

クロージャ

高階関数にはクロージャはつきもの。

  • プログラミング言語の進化を追え:大人のためのブラックボックス読解講座――クロージャとオブジェクトの微妙な関係 (1/2) - ITmedia エンタープライズ

    関数型言語の多くはむしろすべての関数が環境を持つのが当然であり(グローバルな関数はたまたま環境が空であると考える)、そこでは「関数」と「クロージャ」はほぼ同義で使われる。

  • クロージャと高階関数

    クロージャとは単なる関数ではなく、その関数が定義された環境への参照を持った関数のことです。ここでいう環境とは変数とスコープです。 Javascriptでは関数はクロージャになります。そして関数と環境は次のような関係があります。

    1.関数は定義されたときの環境を保持する。
    2.関数が実行されるときに新たな環境(ローカル環境)が生成される。
    3.変数へのアクセスが関数の内部で解決されない場合、関数の保持する環境で解決される。

  • クロージャとラムダ式をザックリ解説してみる | No Creativity, No Life!

    一方でクロージャは『レキシカルスコープ(静的スコープ)と紐付いたファーストクラスオブジェクトの関数』という、なんとも掴みどころのない定義になってしまいがち。...
    次に『レキシカルスコープ(静的スコープ)と紐付いた関数』という部分ですが、これは『引数以外にも、関数自体の宣言場所と同一スコープにあるオブジェクトを持ち運べる関数』と言い換えるとニュアンスが掴み易いのではないかな、と思います。

「クロージャは(関数の)宣言場所と同一スコープにあるオブジェクトを持ち運べます」というところがミソのよう。

クロージャの定義は、

  • Scala - 第3章:クロージャに Challenge! - Qiita

    ● 関数Aの戻り値が関数B
    ● 変数Cは、関数A内で宣言されている
    ● 関数B内では身元不明の変数Cを、宣言なしで使える

    変数Cを自由変数と呼ぶことにすると、
    クロージャとは自由変数を持つ関数と言えます。
    この場合クロージャとは関数Bとなります。

でもって、クロージャの持ち運べるオブジェクトというのが自由変数

追記

以下も参照。


ラムダ式と無名関数と高階関数なんてものが何故必要なの?

いろいろと言葉の定義が難しいが、要は、高階関数とか第一級関数というのは関数の引数に関数を渡したり,関数の戻り値として関数を返せるような関数の事か。

「すべての計算や処理などを関数の定義の組み合わせとして記述していく」には、関数の引数に関数を渡したり,関数の戻り値として関数を返せるような関数が必要。
そのような関数が高階関数とか第一級関数というもので

そんでもって関数の引数や戻り値に関数を指定する時に、わざわざ別の場所で関数を定義するのでは無く、手っ取り早く,その場で無名関数をつくってやれれば、関数にわざわざ名前を付けてやる必要は無いし、その時に無名関数をもっと簡略化した記法(ラムダ式)で記述できれば、コードがゴチャゴチャしないって事か。

で、ラムダ式には関数を簡潔に記述するために、型推論を使って型の指定を省略でるという事。


型クラス

ググッってみたがどうもよくわからない。
どうも、実際に(haskellやScalaなどの)関数型言語なるものを勉強してみないとわからないのかな?

参考になりそうなものを以下に記しておく。

カリー化と部分適用

カリー化部分適用という言葉は内容は似ているが違うものを指すらしい。

  • カリー化 - Wikipedia

    カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
    ...
    カリー化は部分適用と混同されやすい。
    部分適用とは、複数引数ある関数の引数の一部だけに実引数を適用する操作のことで...

なんのこっちゃ。

こっちの方がわかりやすい。

カリー化は、クロージャを使って実装可能という事か。

  • カリー化談義 - あどけない話
    • 部分適用という意味
      これは明らかに間違い
    • 「複数の引数を取る関数」を「一引数を取る関数のチェインに直す」こと
      これはkmizuさんの定義。世間でもよく使われる。
    • 「構造体を一つ取る関数」を「構造体のメンバーを複数の引数にばらし、一引数を取る関数のチェインに直す」こと
      これは僕の定義。というか、Haskellコミュニティの定義。 ...
      複数の引数を取る関数に対して、いくつかの引数を固定できるなら、これは部分適用と言える。 ... カリー化のメリットは、部分適用である。ある関数を雛形として、引数をカスタマイズした関数を作り出せる。

カリー化のメリットは、map関数など高階関数で有効という事か。

まとめてみると

重複するが、蛇足を付け加えてみる。

カリー化部分適用という言葉は同じ事を意味すると思いがちだが、微妙に意味が違うらしい。

カリー化するという事は引数を複数とる関数を引数を一つとる関数に分解する事で、 部分適用するという事は、「複数の引数を取る関数に対していくつかの引数を固定」して適用する(呼び出す)事のよう。

JavaScriptにおける3引数の場合の例をあげると

function func_org(x, y, z) {
    // 何かの処理、例えば3引数の加算であれば
    return x + y + z;
}

という関数を以下のようにクロージャを使って、引数を一つのみとる関数に分解する事ができる。
これをカリー化と言って

function func_curry(x) {
    return function(y) {
        return function(z) {
            return func_org(x, y, z);
        };
    };
}

ここまでがカリー化、すなわち関数を分解するという事。

そうすると、この関数を部分適用する事で
引数x,y,zがそれぞれ1,2,3の場合に

result = func_org(1, 2, 3);

の呼び出しは、

func_curry_1 = func_curry(1);
func_curry_1_2 = func_curry_1(2);
result = func_curry_1_2(3);

と等価になり、これは以下のように記述できるという事。

result = func_curry(1)(2)(3);

また、部分適用は、引数の一部を固定して呼び出す事なので、カリー化(引数が1個)でなくても

function func_part(y) {
    return function(x, z) {
        return func_org(x, y, z);
    };
}

result = func_part(2)(1, 3);

のような形で呼び出す事ができる。

でもってカリー化する事によって分解された関数は部分適用に利用できるので、「カリー化のメリットは、部分適用」という事になるけれども、上記のように部分適用のために使われる関数は必ずしもカリー化によって分解された関数である必要は無いという事か。

そしてカリー化のメリットは、map関数など高階関数で発揮できる。

そういう事だと思う、多分。

でも、それで「カリー化と部分適用の違い」ってそんなに重要な事?
どうでもいいじゃんそんな事と思うんだけど、それって単に俺がアホなだけ?

追記

カリー化のメリットは「物事を単純化できる戻り値は関数なので、使う瞬間に評価(実行)することができる。」という事らしいのだが...

でも、それってそんなに役に立つことなの?
そもそもHaskell や OCamlのような関数型言語をちゃんと学んでみない事にはカリー化のメリットが理解できないのだろうか。

ラムダ計算

ここに、参考になりそうなサイトのリンクが、

このリンク先の記事をいろいろと読んでみた。

  • ラムダ計算 - Wikipedia

    ラムダ計算(らむだけいさん、英: lambda calculus)は、計算模型のひとつで、計算の実行を関数への引数の評価(英: evaluation)と適用(英: application)としてモデル化・抽象化した計算体系である。ラムダ算法とも言う。

ラムダ計算は計算モデル。
なにやら、難しいな!

ちんぷんかんぷん

う~ん、なんだか理解できない。
OCaml言語を知らないから理解できないのかな?

やっぱり無理。
そしたら、以下の記事が比較的わかりやすい。
でもやっぱり根がバカなので後半はよく理解できなかった。

  • ラムダ計算基礎文法最速マスター - 貳佰伍拾陸夜日記

    ラムダ計算の式を項(term)と言います. 項は変数, 抽象, 適用のいずれかです. ...
    変数には関数内の束縛変数(bound variable)か自由変数(free variable)かという区別があります. 関数を値に適用する以外のやり方で変数に値を代入することはできません. ...
    関数を作る構文を抽象(abstraction)と言います...
    関数を呼び出すことを適用(application)と言います 束縛変数の名前が違うだけの項は同じ項として扱われます. ...これをα同値(α-equivalence)と言い, 同値な項に書き換えることをα変換(α-conversion)と言います. ...
    ラムダ計算の式を実行することを簡約(reduction)と言います.

式をと言い、
項は変数, 抽象, 適用のいずれか。

自由変数束縛変数については、

抽象は関数定義そして、適用は関数を呼び出しって事?
引数名だけが異なる関数はα同値という事でα変換という正規化のような事をするのかな。

簡約はラムダ計算の式を実行することそしてβ簡約(β-reduction)と呼ばれるものもあるって事。

JavaScriptを知っているなら、以下の記事がわかりやすい。

  • JavaScriptで学ぶ・プログラマのためのラムダ計算 - 檜山正幸のキマイラ飼育記
    1. 予約語functionの代わりにギリシャ文字のλ(ラムダ)を使う。
    2. return 式; というJavaScript文は、returnを省略して式だけを書く。
    3. 関数定義の本体を囲む波括弧の代わりに丸括弧を使う。
    4. 「λ+仮引数リスト」と「関数定義の本体」のあいだの区切りにピリオドを打つ。

    ...
    引数が1つのときは、引数リストの丸括弧は省略してもかまいません。...
    ラムダ式が、関数を表現するものであることは分かりますね。ラムダ計算とは、要するに関数の計算のことです。...
    適用とは、関数(を表現する式)の直後に付けられた括弧内に別な式を入れることです。...
    適用はあくまで形のうえだけの操作であり、実際の計算や処理が進行することはありません。適用により、2つの式から新しい式が作られるだけです。...
    くどいけど、適用と実際の計算(評価とも呼ぶ)は区別してください。ラムダ計算の適用は、関数に引数を渡した形を作るだけです。...
    、「ラムダ式に別な式を適用しても計算は進行しない」と言いました。では、計算を進行させるにはどうしたらいいでしょうか。ここで、β<ベータ>変換(β還元、あるいは単に還元とも呼ぶ)操作が登場します。β変換により、実際の計算が進行するのです。
    2*2→4 のような変換(書き換え)はβ変換とは呼ばないので注意。組み込み演算子、組み込み関数の実行はラムダ計算とは切り離して考えたほうがいいですね。

ラムダ式が関数を表現するもので、ラムダ計算は関数の計算の事。
適用は、2つの式から新しい式が作られるだけで実際の計算はおこなわれない。
そして実際の計算はβ変換((β還元もしくは単に還元)によっておこなわれるという事。
組み込み演算子、組み込み関数の実行はβ変換と言わないのか。

堅苦しいが、丁寧な説明が、でも、全部読む気力が無いので「2.1 ラムダ記法」の部分を読みかじる。

ここも参考になるかな

ここでラムダ計算については挫折。
どうも数学的な理論は苦手。
これ以上は、難しい専門書でも読まなきゃダメなのかな。
とりあえず、深入りせずにほっといて、次へ進もう。

後で、リベンジ。(無理かな?)

(それぞれのプログラミング言語における)ラムダ式

  • ラムダ式 ‐ 通信用語の基礎知識

    計算を処理して一つの値を返すが、名前がない関数である。
    ラムダ計算をベースに考案された関数型プログラミング言語のLispの基盤となっている。

無名関数の事をいうのかな。

ラムダ式もプログラミング言語によっていろいろと記法が異なっていて、
C#の場合は。

  • 第1回 ラムダ式 - @IT

    ラムダ式(λ式)とは、ひと言でいってしまうと、(厳密には正しくないが)匿名メソッドをより短く記述するための構文である。

計算モデルであるラムダ計算におけるλx. eのような式つまりラムダ式を、実際のプログラミング言語にあうようにアレンジしたのがそれぞれのプログラミング言語におけるラムダ式という事になるのかな。
で、そのラムダ式は名前の付いてない関数であるという事か。

結論のない結論

う~ん!
なんとなくわかったようなわからないような。

とりあえず、わかったような気になって、
試しに、関数型言語でなくても関数型プログラミングは書ける・という事でJavaScriptで関数型プログラミング。

pythonでも関数型プログラミング

でも、やっぱり、実際に関数型言語と言われる言語にさわってみないとよくわからない。

まずは、関数型言語の代名詞たるHaskellを学んでみる必要があるのかな。
そんでもって、Javaプログラマ-ならScalaを,.NET派ならF#に進むべきなのかな?
そろそろ年齢的にオツムがアルツハイマーぎみになってきたけど、また新たな言語を覚えるのはしんどいな。
う~ん!道は険しいぞ!

あいかわらず、ものわかりの悪い愚鈍人であった。

とりあえず調べてみたが、どうも、もやもやと。
もう少しなんかわかったら、再整理!

付録

関数型プログラミングを理解するためには、やっぱ関数型言語をカジってみるかな。
という事で、今後の勉強のために、これから学んでみたい関数型言語の参考になりそうなサイトへのリンクを、

Haskell

haskellは関数型言語の代表選手みたい。

F♯

Scala

OCaml

OCamlをWindowsにインストールしようかなと思ったんだけど、Cygwinが必要なみたいだったのであきらめた。
VMwareでもいれて、バージャルな環境でLinuxで試してみようかな。

ページのトップへ戻る