web-dev-qa-db-ja.com

関数型プログラミングのコンテキストで構成可能性とはどういう意味ですか?

関数型プログラマーは、特定のものが構成可能または構成不可能であると言うとき、どういう意味ですか?

私が読んだこの種のステートメントのいくつかは次のとおりです。

  • 制御構造は構成できません。
  • スレッドは構成しません。
  • モナディック演算は合成可能です。
61
Surya

Marcelo Cantosがかなり良い説明をしてくれました ですが、もう少し正確にできると思います。

あるタイプのモノは、いくつかのインスタンスを特定の方法で組み合わせて同じタイプのモノを生成できる場合に構成可能です。

制御構造の構成可能性。Cなどの言語は、を区別します。新しい式、およびステートメントを生成します。これらは、ifforなどの制御構造と、単純に実行する「シーケンス制御構造」を使用して構成できます。順番にステートメント。この配置についての重要な点は、これら2つのカテゴリが同じ根拠にないことです-多くの制御構造は式(たとえば、実行するブランチを選択するためにifによって評価される式)を利用しますが、式は利用できません制御構造(たとえば、forループを返すことはできません)。 「forループを返す」ことを望むのはおかしい、または無意味に思えるかもしれませんが、実際には、制御構造を格納して渡すことができるファーストクラスのオブジェクトとして扱うという一般的な考えは、可能であるだけでなく便利です。 Haskellのような遅延関数型言語では、ifforのような制御構造は通常の関数として表すことができ、他の用語と同様に式で操作できるため、「ビルドする関数などを可能にします。 "渡されたパラメーターに従って他の関数を呼び出し、呼び出し元に返します。したがって、C(たとえば)は「プログラマーがしたいこと」を2つのカテゴリーに分け、これらのカテゴリーのオブジェクトを組み合わせる方法を制限しますが、Haskell(たとえば)は1つのカテゴリーのみを持ち、そのような制限はありませんなので、この意味で、それはより多くの構成可能性を提供します。

スレッドの構成可能性。Marcelo Cantosが行ったように、ロック/ミューテックスを使用するスレッドの構成可能性について本当に話していると思います。表面的には複数のロックを使用するスレッドをできるため、これは少しトリッキーなケースです。しかし重要な点は、複数のロックを使用するスレッドが、それらが意図されていることを保証することはできないということです。

ロックは、特定の保証が付いた、特定の操作を実行できるタイプのロックとして定義できます。 1つの保証は次のとおりです。ロックオブジェクトxがあるとすると、lock(x)を呼び出すすべてのプロセスが最終的にunlock(x)を呼び出す場合、lock(x)への呼び出しは最終的に現在のスレッド/プロセスによってxがロックされた状態で正常に戻ります。この保証により、プログラムの動作に関する推論が大幅に簡素化されます。

残念ながら、世界に複数のロックがある場合、それはもはや真実ではありません。スレッドAがlock(x); lock(y);を呼び出し、スレッドBがlock(y); lock(x);を呼び出す場合、Aがロックxを取得し、Bがロックyを取得し、両方が無期限に待機する可能性があります。他のロックを解放する他のスレッド:デッドロック。したがって、ロックは構成可能ではありません。複数を使用する場合、その重要な保証がまだ保持されていると単純に主張することはできません-コードを詳細に分析してロックを管理する方法を確認しない限り、。言い換えれば、関数を「ブラックボックス」として扱う余裕はもうありません。

abstractionsを有効にするため、構成可能なものは優れています。つまり、すべての詳細を気にすることなくコードについて推論することができ、プログラマーの認知的負担が軽減されます。 。

53
j_random_hacker

構成可能性の簡単な例はLinuxコマンドラインです。パイプ文字を使用すると、単純なコマンド(ls、grep、catなど)を事実上無制限に組み合わせることができ、それにより、多数の複雑な動作を「構成」できます。少数の単純なプリミティブ。

構成可能性にはいくつかの利点があります。

  • より均一な動作:例として、「一度に1ページずつ結果を表示する」(more)を実装する単一のコマンドを使用することで、すべてのコマンドを実装する場合には不可能なページングの均一性が得られます。ページングを行うための独自のメカニズム(およびコマンドラインフラグ)。
  • 繰り返しの少ない実装作業(DRY):ページングの多数の異なる実装を持つ代わりに、どこでも使用される実装は1つだけです。
  • 一定の実装労力でより多くの機能性:既存のプリミティブを組み合わせて、同じ努力がモノリシックで合成不可能なコマンドの実装に当てはまる場合よりもはるかに広い範囲のタスクを解決できます。

Linuxコマンドラインの例が示すように、コンポーザビリティは必ずしも関数型プログラミングに限定されているわけではありませんが、概念は同じです。制限されたタスクを実行する小さめのコードを用意し、出力と入力を適切にルーティングすることでより複雑な機能を構築します。

ポイントは、関数型プログラミングがこれに適しているということです。不変変数と副作用の制限により、呼び出される関数の内部で何が起こるかを心配する必要がないため、より簡単に構成できます。特定の操作シーケンス、または共有ロックへのアクセスでは無効になるため、特定の呼び出しシーケンスでデッドロックが発生します。

これは関数型プログラミングの構成可能性です。どの関数も入力パラメーターにのみ依存し、出力は戻り値の型を処理できる関数に渡すことができます。

拡大すると、データ型が少なくなると構成性が向上します。 Clojureのリッチヒッキーは、

すべての新しいオブジェクトタイプは、これまでに作成されたすべてのコードと本質的に互換性がありません

これは確かによくできたポイントです。

実用的な構成可能性は、Unixコマンドが「タブ区切りの行ベースのテキスト」標準で行うように、データタイプの小さなセットでの標準化にも依存します。

追記

エリック・レイモンドは、Unix哲学について本を書きました。彼が挙げた2つの設計原則は、

  • モジュール性のルール:クリーンなインターフェースで接続された単純なパーツを記述します。
  • 構成のルール:他のプログラムと接続するプログラムを設計します。

から http://catb.org/~esr/writings/taoup/html/ch01s06.html#id2877537

関数型プログラミングの構成可能性は、それらの原則を個々の関数のレベルにまで下げると言えます。

32
j-g-faustus

コンピュータサイエンスの構成は、より単純な動作を集約することによって複雑な動作を組み立てる能力です。関数分解はこの例であり、複雑な関数は、より小さな簡単に把握できる関数に分割され、トップレベルの関数によって最終的なシステムに組み込まれます。トップレベル関数は、ピースを全体に「構成」したと言えます。

特定の概念は簡単に構成できません。たとえば、スレッドセーフなデータ構造では、要素を安全に挿入および削除できます。これは、データ構造またはそのサブセットをロックすることにより、1つのスレッドが変更を邪魔されることなく必要な操作を実行できるようにします。データ構造が破損しています—機能している間ただし、ビジネス関数では、あるコレクションから要素を削除してから、別のコレクションに挿入し、操作全体をアトミックに実行する必要がある場合があります。問題は、ロックがデータ構造ごとにのみ発生することです。要素を1つから安全に削除できますが、いくつかのキー違反のため、要素を他の要素に挿入できない場合があります。または、1秒に挿入して最初から削除してみてください。鼻の下から別のスレッドが盗んだことを確認するだけです。操作を完了できないことに気づいたら、元の状態に戻すことができますが、同様の理由で逆転が失敗することを確認するだけで済みます。もちろん、複数のデータ構造をカバーする、よりリッチなロックスキームを実装することもできますが、これは、すべての操作が単一のものであっても、全員が新しいロックスキームに同意し、常にそれを使用する負担を負う場合にのみ機能します。データ構造。

したがって、Mutexスタイルのロックは、構成されない概念です。低レベルのスレッドセーフ操作を集約するだけでは、高レベルのスレッドセーフ動作を実装することはできません。この場合の解決策は、 [〜#〜] stm [〜#〜] など、作成を行う概念を使用することです。

22
Marcelo Cantos

Marcelo Cantosの答えには同意しますが、一部の読者が持っているよりも多くの背景を想定している可能性があります。これは、関数型プログラミングの構成が特別である理由にも関連しています。関数型プログラミングの関数構成は、数学の関数構成と基本的に同じです。数学では、関数f(x) = x^2と関数g(x) = x + 1を使用できます。関数の構成とは、新しい関数を作成することを意味します。関数の引数は「内部」関数に渡され、「内部」関数の出力は「外部」関数への入力として機能します。 f outerをg innerで構成すると、f(g(x))と書くことができます。 xに_1_の値を指定すると、g(1) == 1 + 1 == 2、つまりf(g(1)) == f(2) == 2^2 == 4になります。より一般的には、f(g(x)) == f(x + 1) == (x+1)^2f(g(x))構文を使用して構成を説明しましたが、数学者は別の構文_(f . g)(x)_を好むことがよくあります。これは、_f composed with g_自体が単一の引数を取る関数であることを明確にしているためだと思います。

関数型プログラムは、完全に構成要素プリミティブを使用して構築されています。 Haskellのプログラムは、過度に単純化するために、ランタイム環境を引数として取り、その環境の操作の結果を返す関数です。 (このステートメントを理解するには、モナドをある程度理解する必要があります。)それ以外はすべて、 数学的な意味での合成 を使用して行います。

5
Aidan Cully

別の例:.NETでの非同期プログラミングを検討してください。 C#のような言語を使用していて、Begin/End APIを介して一連の非同期(非ブロッキング)I/O呼び出しを行う必要がある場合、2つの操作FooBarを順に呼び出すには、2つの操作を公開する必要があります。メソッド(BeginFooAndBarEndFooAndBar)、ここでBeginFooAndBarBeginFooを呼び出してIntermediateにコールバックを渡し、次にIntermediateBeginBarを呼び出します。中間値とIAsyncResult状態情報をスレッド化する必要があり、try-catch幸運なことに、例外処理コードを3か所で複製する必要があります。それはひどい混乱です。

しかし、F#では、async型があり、合成可能な関数の継続の上に構築されているので、たとえば、.

let AsyncFooAndBar() = async {
    let! x = Async.FromBeginEnd(BeginFoo, EndFoo)
    let! y = Async.FromBeginEnd(BeginBar, EndBar, x)
    return y * 2 }

または何があり、それが単純で、try-catchをその周りに配置したい場合は、コードが3つに分散するのではなく、1つのメソッドにまとめられている場合、try-_catchを配置するだけで機能します。

2
Brian

構成可能性により、開発者コミュニティは、ベースレイヤーにチェーンすることなく、継続的に抽象化のレベル、複数のレベルを上げることができます。

1
Ted Bradley

これが実際の例です。あなたの家に住んでいるすべての人の名前は、あなたの家のすべての男性の名前とあなたの家のすべての女性のリストを組み合わせたリストです。

これらの2つのサブ問題はそれぞれ独立して、他の問題の解決を妨げることなく解決できるため、これは構成可能です。

一方、ステップは特定の順序で実行し、他のステップの結果に依存する必要があるため、多くのレシピは構成できません。泡立てる前に卵を割る必要があります!

1
kyoryu