副作用のあるクロージャーは「機能的なスタイル」と見なされますか?


9

最新のプログラミング言語の多くは、クロージャ、つまりコード(ブロックまたは関数)の概念をサポートしています。

  1. 値として扱うことができるため、変数に格納し、コードのさまざまな部分に渡し、プログラムの一部で定義し、同じプログラムのまったく異なる部分で呼び出すことができます。
  2. 変数が定義されているコンテキストから変数をキャプチャし、後で呼び出されたときに(おそらくまったく異なるコンテキストで)変数にアクセスできます。

以下は、Scalaで記述されたクロージャーの例です。

def filterList(xs: List[Int], lowerBound: Int): List[Int] =
  xs.filter(x => x >= lowerBound)

関数リテラルx => x >= lowerBoundには、同じ名前のlowerBound関数の引数によって閉じられている(バインドされている)自由変数が含まれていますfilterList。クロージャはライブラリメソッドに渡さfilterれ、通常の関数として繰り返し呼び出すことができます。

私はこのサイトで多くの質問と回答を読んでいますが、私が理解している限り、閉鎖という用語は関数型プログラミングと関数型プログラミングスタイルに自動的に関連付けられることがよくあります。

ウィキペディアの関数プログラミングの定義は次のとおりです。

コンピュータサイエンスでは、関数型プログラミングは、計算を数学関数の評価として扱い、状態や変更可能なデータを回避するプログラミングパラダイムです。状態の変化を強調する命令型プログラミングスタイルとは対照的に、関数の適用を強調します。

そしてさらに

[...]関数コードでは、関数の出力値は関数に入力された引数にのみ依存します[...]。副作用をなくすことで、プログラムの動作の理解と予測がはるかに容易になります。これは、関数型プログラミングを開発するための主要な動機の1つです。

一方、プログラミング言語によって提供される多くのクロージャー構成では、クロージャーが非ローカル変数をキャプチャーし、クロージャーが呼び出されたときにそれらを変更できるため、それらが定義された環境に副作用が生じます。

この場合、クロージャは関数型プログラミングの最初のアイデアを実装します(関数は他の値のように移動できるファーストクラスのエンティティです)が、2番目のアイデアは無視します(副作用を回避します)。

副作用のあるこのクロージャの使用は、関数型スタイルと見なされますか、それとも、関数型と非関数型プログラミングスタイルの両方に使用できるより一般的な構造と見なされますか?このトピックに関する文献はありますか?

重要な注意点

副作用の有用性や副作用のあるクロージャーの有用性については質問していません。また、副作用の有無にかかわらず、クロージャーの長所/短所についての議論には興味がありません。

このようなクロージャの使用が関数型プログラミングの支持者によって依然として関数型スタイルと見なされているかどうか、または逆に関数型スタイルを使用する場合にそれらの使用が推奨されていないかどうかを知りたいだけです。


3
純粋に機能的なスタイル/言語では、副作用は不可能です...だから、それは問題だと思います:コードが機能的であると考える純度のレベルはどれくらいですか?
Steven Evers

@SnOrfus:100%ですか?
Giorgio

1
純粋な関数型言語では副作用は不可能であるため、これであなたの質問に答えられるはずです。
Steven Evers

@SnOrfus:多くの言語では、クロージャーは関数型プログラミングの構成要素と見なされているため、少し混乱していました。(1)それらの使用法はFPを実行することよりも一般的であり、(2)クロージャを使用しても、関数スタイルの使用を自動的に保証するものではないように思えます。
ジョルジョ

反対投票者はこの質問を改善する方法についてメモを残すことができますか?
ジョルジオ

回答:


7

番号; 機能的パラダイムの定義は、状態の欠如と暗黙の副作用の欠如についてです。高次関数、クロージャー、サポートされる言語のリスト操作、その他の言語機能についてではありません...

関数型プログラミングの名前の由来の機能の数学的概念 - 同じ入力の繰り返し呼び出しが常に同じ出力できます nullipotent機能を- 。これは、データが不変である場合にのみ達成できます。開発を容易にするために、関数は変更可能になり(関数は変更され、データは依然として不変)、高次関数(たとえば、数学の関数、導関数など)の概念-入力として別の関数を使用する関数。関数が持ち越されて引数として渡される可能性のために、ファーストクラスの関数が採用されました。これらに続いて、生産性をさらに高めるために、クロージャーが登場しました。

もちろん、これは非常に単純化されたビューです。


3
高次関数は、可変性とはまったく関係がなく、ファーストクラスの関数も関係ありません。変更可能な状態や副作用なしに、Haskellで関数を渡して他の関数を簡単に構築できます。
tdammers

@tdammers私はおそらく非常にはっきりと表現していませんでした。関数が可変になると言ったとき、私はデータの可変性についてではなく、関数の動作を(高次の関数によって)変更できるという事実について言及しました。
m3th0dman

高階関数は既存の関数を変更する必要はありません。ほとんどの教科書の例では既存の関数をまったく変更せずに新しい関数に結合してmapいます。たとえば、関数を取得し、それをリストに適用して、結果のリストを返します。mapは引数を変更しません。引数として受け取る関数の動作は変更しませんが、これは間違いなく高次関数です。関数パラメーターのみを使用して部分的に適用すると、リストを操作する新しい関数ですが、まだ変更は行われていません。
tdammers

@tdammers:まさに:可変性は命令型またはマルチパラダイム言語でのみ使用されます。これらはより高次の関数(またはメソッド)とクロージャーの概念を持つことができますが、不変性を放棄します。これは便利かもしれません(私はそうするべきではないと言っているわけではありません)が、私の質問は、この機能をまだ呼び出すことができるかどうかです。つまり、一般的に、クロージャは厳密に機能する概念ではありません。
Giorgio

@Giorgio:古典的なFPの言語のほとんどが行う可変性を持っています。Haskellは、頭に浮かぶと考えられる中で、可変性をまったく許可しない唯一のものです。それでも、可変状態を回避することはFPの重要な値です。
tdammers

9

いいえ。「関数型」とは、副作用のないプログラミングを意味します。

理由を確認するには、拡張メソッドに関するEric LippertのブログエントリForEach<T>と、MicrosoftがLinqにこのようなシーケンスメソッドを含めなかった理由をご覧ください。

私は、2つの理由から、そのような方法を提供することに哲学的に反対しています。

最初の理由は、これを行うと、他のすべてのシーケンス演算子が基づく関数型プログラミングの原則に違反するためです。明らかに、このメソッドの呼び出しの唯一の目的は、副作用を引き起こすことです。式の目的は、副作用を引き起こさないように値を計算することです。ステートメントの目的は、副作用を引き起こすことです。このことの呼び出しサイトは、式のようにひどく見えます(確かに、メソッドがvoidを返すため、式は「ステートメント式」のコンテキストでのみ使用できます)。副作用にのみ役立つ唯一のシーケンス演算子を作成します。

2番目の理由は、そうすることで言語に新しい表現力が追加されないためです。これを行うと、この完全に明確なコードを書き換えることができます。

foreach(Foo foo in foos){ statement involving foo; }

このコードに:

foos.ForEach((Foo foo)=>{ statement involving foo; });

ほぼ同じ文字をわずかに異なる順序で使用します。さらに、2番目のバージョンは理解が難しく、デバッグが難しく、クロージャセマンティクスが導入されているため、オブジェクトのライフタイムが微妙に変化する可能性があります。


1
しかし、私は彼に同意します...彼の議論は検討するときに少し弱まりParallelQuery<T>.ForAll(...)ます そのようなの実装は、ステートメントのIEnumerable<T>.ForEach(...)デバッグに非常に役立ちますForAllForAllwith ForEachを置き換え、を削除するAsParallel()と、より簡単にステップスルー/デバッグできます)
Steven Evers

明らかに、Joe Duffyは異なる考えを持っています。:D
Robert Harvey

Scalaは純粋性も強制しません。純粋でないクロージャを高次関数に渡すことができます。私の印象は、クロージャーの概念は関数型プログラミングに固有のものではなく、より一般的な概念であるということです。
Giorgio

5
@ジョルジオ:クロージャは、クロージャと見なされるために純粋である必要はありません。ただし、「機能的なスタイル」と見なされるには、純粋である必要があります。
Robert Harvey

3
@ジョルジオ:変更可能な状態と副作用の周りにクロージャーを作成し、それを別の関数に渡すことができると本当に便利です。これは、関数型プログラミングの目的とは正反対です。関数型言語では一般的なラムダに対するエレガントな言語サポートが原因で、多くの混乱が生じていると思います。
GlenPeterson

0

関数型プログラミングはファーストクラスの関数を次の概念レベルに確実に導きますが、匿名関数を宣言したり、関数を他の関数に渡したりすることは、必ずしも関数型プログラミングではありません。Cでは、すべてが整数でした。数値、データへのポインタ、関数へのポインタ...すべてintです。関数ポインタを他の関数に渡したり、関数ポインタのリストを作成したりできます。まあ、アセンブリ言語で作業する場合、関数は実際にはマシン命令のブロックが格納されているメモリ内の単なるアドレスです。関数に名前を付けると、コンパイラーがコードを作成する必要がある人にとって余分なオーバーヘッドになります。つまり、関数は、その意味では完全に非関数型の言語では「ファーストクラス」でした。

REPLで数式を計算するだけの場合は、言語を純粋に機能させることができます。しかし、ほとんどのビジネスプログラミングには副作用があります。長期実行プログラムが完了するのを待つ間にお金を失うことは、副作用です。ファイルへの書き込み、データベースの更新、イベントのログ記録などの外部アクションを実行するには、状態を変更する必要があります。これらのアクションを副作用をプッシュオフする不変のラッパーにカプセル化して、コードがそれらについて心配する必要がない場合、状態が本当に変更されるかどうかについて議論することができます。でも、森に落ちて、誰も聞いていない森に落ちたとき、木が音を立てるかどうかを話し合うようなものです。事実は、木が直立して地面に着いたことです。物事が完了したことを報告するだけであっても、物事が完了すると状態が変更されます。

したがって、機能的な純粋さのスケールが残されています。白黒ではなく、グレーの色合いです。そして、その規模では、副作用が少なく、変異性が少ないほど、優れています(より機能的)。

他の機能を備えたコードで副作用や変更可能な状態が絶対に必要な場合は、できるだけカプセル化するか、プログラムの他の部分から変更できるように努めます。クロージャー(または他の何か)を使用して、副作用または変更可能な状態を純粋な関数に注入することは、関数型プログラミングの正反対です。唯一の例外は、クロージャーが渡されたコードからの副作用をカプセル化する最も効果的な方法であった場合です。それはまだ「関数型プログラミング」ではありませんが、特定の状況で入手できるクローゼットかもしれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.