web-dev-qa-db-ja.com

命令型言語で関数型パラダイムを使用することは可能ですか?

概念を正しく理解していれば、関数型言語が達成しようとしている目標は、関数からの副作用を排除し、状態を排除することです。この背後にある理論的根拠は、より予測可能な実行につながる参照透過性を取得することです。

しかし、命令型言語で上記を念頭に置いてコードを書くことを妨げるものは何もありません。機能的なミックスインではなく、古典的な構成についてのみ考えています。

Cに次のコードがあるとしましょう。

int add(int x, int y) {
    return x + y;
}

int sqr(int x)
{
    return x*x;
}

int main()
{
    return sqr(add(3,5));
}

だから、私には副作用がない2つの機能があります。どちらのプログラムにも状態はありません。このコードには、非常に機能的なものがありませんか?

現在、関数型プログラミングのコアコンセプトに糖衣構文で作られた印象的な装飾を構築したかのように関数型言語を認識しています。彼らの構文は、コードをステートメントにスライスすることを思いとどまらせますが、命令型言語で関数型アプローチをとる(そしてその結果を生む)のを妨げる大きな違いはないと思います。私が間違っている?したがって、私の質問です。

2
doc

命令型言語で純粋に関数型のスタイルでプログラミングすることは確かに可能です。実際、Effective Java or Java Concurrency in Practiceのような本を見ると、これらの本のアドバイスの多くは基本的に「可変状態を使用する」、「副作用を使用しない」など。

しかし、それは必ずしも楽しい経験ではないかもしれません、そしてあなたはあなたがしたいすべてをすることができないかもしれません。

例として具体的にCについて言及しました。何かが足りない場合を除いて、反復を表現する方法がないため、Cの純粋関数サブセットはチューリング完全ではありません。 Cには適切な末尾呼び出しがないため、無限リストを反復処理するようなものを表現しようとすると、最終的にスタックがオーバーフローします。反復にはループを使用する必要がありますが、ループは変更可能な状態と副作用に依存しています。

もちろん、標準のチューリング陥穈の議論が適用されます…CでHaskellインタープリターを実装してからHaskellでプログラミングすることにより、関数型プログラミングCを行うことができます。しかし、それは私がOPの質問を解釈する方法ではありません。

4
Jörg W Mittag

関数型プログラミングとは、不変値のみを使用したプログラミングを意味する場合は、それを実行できます。しかし、それは苦痛になるでしょう。多くの場合、以下を利用することはできません。

  • 字句スコープを備えたファーストクラスの関数(別名クロージャ)
  • ほとんどが特殊文字である識別子を持つ関数
  • 中置関数
  • 型推論
  • 末尾呼び出しの最適化(スタックオーバーフローに問題がない限り、ループの形式として再帰はありません。つまり、ループstatementsを使用する必要があります。つまり、結果を取得するには何かを変更する必要がありますループ)
  • 自動カレー
  • if/elseおよびtryステートメントの代わりに。
  • 代数的データ型
  • パターンマッチング
  • nullに対処する必要がない
  • 永続データ構造(通常、これらのライブラリが利用可能な場合は、外部ライブラリを取り込む必要があります)
  • 高度なモジュールシステム
  • 遅延評価(これはラムダ式を使用してシミュレートできますが)

そしてもちろん、関数型言語のコンパイラーは、命令型言語のコンパイラーとは異なる最適化を行います。関数がプリミティブ型ではない言語や、関数がプリミティブ型である言語は、関数の合成やカレー化を最適化する可能性が低く、頻繁に構成されてカレー化されることが予想されます。

13
Doval

[...]しかし、命令型言語で関数型アプローチをとること(およびその成果を生み出すこと)を妨げるような大きな違いはありません。

CやC++のような言語でそれらの「果物」のいくつかを楽しむことを試みる私の実用的な種類の方法は、すべてを機能させることではありません。とてもリラックスしています。私はあちこちで高次関数を記述したり、クロージャーを利用したり、命令型ループやローカルカウンター変数などを回避したりしていません。私はこれらの言語とあまり戦おうとはしていません。もちろん、ラムダと高階関数が特定の汎用C++アルゴリズム(標準ライブラリの例を含む)に自然に適合する場合があり、そのように指先から流れ出るように見えるときに使用しますが、私はmそのような言語で機能的なスタイルを強制しようとはしていません。

ほとんどの場合、私は外部の副作用を排除しようとしています。または、実際のプログラムには副作用(および時には複雑な一連の副作用)がしばしば必要であることを指摘する必要があると感じている人はcentralize最も少ない数の場所への副作用。私は外部の副作用を「スレッドの呼び出しスタックの最下部」に移動しようとします。これは、それについて説明/考える非常に大雑把な方法ですが、私の目的には実用的だと思います。

そして、並列化の機会を増やし、スレッドセーフなどについて推論するのが簡単になることに加えて、私が見つけた最も実用的な利点は、自分や他の人の作成の複雑さにほとんど圧倒されていないことです。これにより、脳が非常に多くの詳細を繰り返し理解することを余儀なくされて爆発の危機に瀕しているように感じることなく、より大規模な設計上の懸念に集中することができます。これは、何が起こっているのかを理解するために作成した抽象概念をX線で撮影する必要がないようにするために欠けていた主要な欠けているパズルピースの1つでした。

オブジェクト間で複雑な一連の関数呼び出しまたはメソッド呼び出しがあり、それらの多くが外部の副作用(メンバー変数、関数に渡されるパラメーター、またはグローバル、yuckなど)を持っている場合、必然的にケース(バグが発生してテストをエスケープした場合に最も明白ですが、変更を加えたり、システム内に新しい機能を組み込んだりする場合も同様です)を深く掘り下げ、すべてをつなぎ合わせて、関連する外部(例:アプリケーション)状態に発生しています。同様の一連の呼び出しがデータの入力と出力のみであり、他の何かが変異していない場合、脳が追跡する必要のある情報がはるかに少なくなり、(健全なテスト手順と組み合わせると)飛行中の間違いがはるかに少なくなります。レーダーの下で。

そうは言っても、私はこれについて非常にリラックスしています。このようなことをしても大丈夫です:

// 'some_mesh' is passed by value (copied, not passed by ref/pointer)
static Mesh modify_mesh(Mesh some_mesh)
{
     // Transform some_mesh using imperative statements.
     ...

     // Output new, modified mesh.
     return some_mesh;
}

some_meshはこのmodify_mesh関数のローカルであり、他の場所に渡されたり変更されたりせず、関数には外部の副作用がなく、参照の透過性があるため、このようなことで大丈夫です。たぶん、add_trianglesのようないくつかの変更メソッド、またはそのローカルメッシュオブジェクトに副作用/変更を引き起こすものを呼び出します。コールスタックの深さ約20レベルでこのメッシュを通過させず、何が起こっているのか、どのような正確な順序で追跡しようとしているかに圧倒されている間は、それを変更しない限り、問題ありません。

そして、私は実際に、アトミックな参照カウント、不変のインターフェイス、ビルダー、この種のものを使用していくつかの永続的なデータ構造を構築しました。コピーするのに非常に費用がかかるものと、ソフトウェアが特に好むものはそうではありません。一連の関数呼び出し間およびスレッド間での変更(すべてを格納する中央アプリケーションsceneは現在不変であり、操作で実行できるのは、既存のシーンを変更するのではなく、大規模な置換のために新しいシーンを出力することだけです)。しかし、あなたがうまくいけば見ることができるように、ほとんどそれは非常にリラックスしています。私の全体的な目標はこれを回避することなので、私は脳が追跡しなければならない情報の量を減らしようとしています。

「コンピューティング科学者の主な課題は、彼自身の作成の複雑さに混乱しないことです。」 -― Edsger W. Dijkstra

...ますます大規模なソフトウェアを構築するにつれて。

副次的なボーナスとして、私は物事をマルチスレッド化する機会がはるかに多いことも発見しました。たとえば、物理学を中央のシーンを変更し、レンダリングをロックする方法で同時に読み取りたいと考えていたので、リアルタイムレンダリングと並行して物理学を実行することを検討することさえこれまで考えたことはありませんでした。スレッドを使用して個別に終了するよりもコストがかかる場合があります。現在、物理学は中心的なシーンを変更しません。 1つを入力し、新しいものを出力します。レンダラーがシーンを入力し、軽量のコピーを保持し(シーンがPDSであるため)、それをレンダリングしている間、スレッドをできるだけ速く実行することができます。独自のスレッドでできるだけ速くそれを行います。複数のスレッドを使用してループなどをシーケンシャルパイプラインで並列化して、これらすべてをより速く終了させようとする前に(例:物理が並列ループでより速く終了し、レンダラーが同じスレッドでレンダリングした後、終了しましたが、世の中でこれらのことを気にせずに並行して実行すると、結果のコードが単純化されるだけでなく、ユーザーにとってはるかにスムーズで高速で一貫したフレームレートに変換されます。しかし、それは副次的なボーナスです。私は主に、システム全体を理解し、より明確な感覚を実現するために、副作用の軽減を模索していました。

その例外に加えて、安全性とエラー処理は今では簡単です。特に永続的なアプリケーション状態に対する外部の副作用が関係する前は、例外からの回復の最も複雑な部分は、それらの副作用をロールバックすることでした。現在、システムの大部分の機能にはそのような副作用はありません。何かがスローされた場合、ロールバックするものはありません。元に戻す/やり直しシステムも、シーン全体をコピーするだけなので、とんでもないほど簡単になりました(PDSであるため、メモリをほとんど消費しません)。非破壊編集は簡単です。などなど。

0
Dragon Energy

私は関数型言語とパラダイムの絶対的な専門家ではありません。ただし、コンパイラには、ネイティブ言語(C、Ada、Prolog、Java…)をマシン言語(x86、JVM、sparc、AMD64…)に変換する仕事があることは知っています。関数型言語と命令型言語はどちらもマシンコードに変換できるため、チューリング完全でドメイン固有ではないことが前提です。したがって、それらは相互に変換できます。だから、私はそう思うでしょう。

私は今学んでいますScala、そしてそれは両方を可能にします。Cのような標準的な中括弧を使用して命令的に書くか、中括弧を省略して機能的に書くことができます。明らかにあなたが使用する関数純粋に機能的に書くことができるかどうかを決定しますが、それは可能です。

良い比較例を考えたら、ラップトップに乗ったときに編集します。

0
Carcigenicate