一部の言語(Haskell?)では、ポイントフリースタイルを実現すること、または関数の引数を名前で明示的に参照しないことを目指していることを私は知っています。これは私が習得するのは非常に難しい概念ですが、そのスタイルの長所(またはおそらく短所)が何であるかを理解するのに役立つかもしれません。誰か説明できますか?
4 に答える
ポイントフリースタイルは、究極の関数型プログラミングスタイルと一部の作者によって考えられています。簡単に言うと、型の関数は、型のある要素から型の別の要素へt1 -> t2
の変換を記述します。「ポイントフル」関数(変数を使用して記述)は要素を強調し(記述すると、要素に何が起こっているかを記述します)、「ポイントフリー」関数(変数を使用せずに表現)は変換自体を強調します。より単純な変換の構成。ポイントフリースタイルの支持者は、変換は確かに中心的な概念であるべきであり、ポイントフリー表記は使いやすいが、t1
t2
\x -> ... x ...
x
ポイントフリー関数型プログラミングは非常に長い間利用可能でした。これは、1924年のMosesSchönfinkelによる独創的な研究以来、コンビネータ論理を研究してきた論理学者によってすでに知られており、1950年代にRobertFeysとHaskellCurryによってML型推論になるものに関する最初の研究の基礎となっています。
基本的なコンビネーターの表現力豊かなセットから関数を構築するというアイデアは非常に魅力的であり、 APLから派生した配列操作言語や、HaskellのParsecなどのパーサーコンビネーターライブラリなど、さまざまなドメインに適用されています。ポイントフリープログラミングの注目すべき支持者はJohnBackusです。彼の1978年のスピーチ「プログラミングはフォンノイマンスタイルから解放されることができるか?」で、彼は次のように書いています。
ラムダ式(およびその置換規則)は、すべての可能なタイプおよび任意の数の引数のすべての可能な計算可能関数を定義することができます。この自由と力には、明らかな利点だけでなく、欠点もあります。これは、従来の言語での無制限の制御ステートメントの力に類似しています。無制限の自由には混乱が生じます。ラムダ計算のように、機会に合わせて新しい結合形式を絶えず発明する場合、すべての目的に適したいくつかの結合形式のスタイルや有用な特性に慣れることはできません。構造化プログラミングが多くの制御ステートメントを避けて、より単純な構造、より優れたプロパティ、およびそれらの動作を理解するための統一されたメソッドを備えたプログラムを取得するのと同じように、関数型プログラミングはラムダ式、置換、および複数の関数タイプ。これにより、既知の有用なプロパティを備えた使い慣れた関数形式で構築されたプログラムを実現します。これらのプログラムは非常に構造化されているため、高校の代数問題の解決に使用されるものと同様の代数手法を機械的に使用することで、その動作を理解および証明できることがよくあります。
だからここにあります。ポイントフリープログラミングの主な利点は、方程式の推論を自然にする構造化されたコンビネータースタイルを強制することです。等式推論は、「Squiggol」運動の支持者によって特に宣伝されており([1] [2]を参照)、実際、ポイントフリーのコンピュテーションと計算/書き換え/推論ルールのかなりの部分を使用しています。
- [1] 「Bird-MerteensFormalismの紹介」、Jeremy Gibbons、1994年
- [2] 「バナナ、レンズ、封筒、有刺鉄線を使った関数型プログラミング」、Erik Meijer、Maarten Fokkinga、Ross Paterson、1991年
最後に、ハスケライトの間でポイントフリープログラミングが人気の理由の1つは、圏論との関係です。圏論では、射(「オブジェクト間の変換」と見なすことができる)が研究と計算の基本的なオブジェクトです。部分的な結果により、特定のカテゴリでの推論をポイントフリースタイルで実行できますが、矢印を作成、調査、操作する一般的な方法はポイントフリースタイルであり、文字列図などの他の構文でもこの「ポイントフリー」が示されます。「プログラミングの代数」手法を提唱する人々とプログラミングのカテゴリーのユーザーの間にはかなり緊密な関係があります(たとえば、バナナの論文[2]の著者はハードコアなカテゴリー主義者です)。
HaskellwikiのPointfreeページに興味があるかもしれません。
ポイントフリースタイルの欠点はかなり明白です。読むのは本当に苦痛かもしれません。シャドウイングやアルファ等価などの多くの恐怖にもかかわらず、変数を使用するのが好きな理由は、それが読んで考えるのがとても自然な表記法だからです。一般的な考え方は、複雑な関数(参照透過性の言語)は複雑な配管システムのようなものです。入力はパラメーターであり、いくつかのパイプに入り、内部関数に適用され、複製(\x -> (x,x)
)または忘れられます()\x -> ()
、パイプはどこにも通じていません)など。そして、変数表記は、そのすべての機械についてうまく暗黙的です。入力に名前を付け、出力(または補助計算)に名前を付けますが、すべての配管を記述する必要はありません。小さなパイプが大きなパイプなどの邪魔にならないように計画します\(f,x,y) -> ((x,y), f x y)
。驚くべき短いものの内部の配管の量。各変数を個別に追跡することも、各中間配管ノードを読み取ることもできますが、機械全体を一緒に見る必要はありません。ポイントフリースタイルを使用する場合、すべての配管が明示的であり、すべてを書き留めて、後でそれを確認する必要があります。場合によっては、単純に醜いこともあります。
PS:この配管のビジョンは、スタックプログラミング言語と密接に関連しています。スタックプログラミング言語は、おそらく(ほとんど)使用されていないプログラミング言語です。私はそれを感じるためだけにそれらの中でいくつかのプログラミングを試みることをお勧めします(私は論理プログラミングをお勧めします)。Factor、Catまたは由緒あるForthを参照してください。
目的は簡潔であり、パイプライン化された計算を、引数をスレッド化することを考えるのではなく、関数の合成として表現することであると私は信じています。簡単な例(F#)-与えられた:
let sum = List.sum
let sqr = List.map (fun x -> x * x)
次のように使用されます:
> sum [3;4;5]
12
> sqr [3;4;5]
[9;16;25]
「二乗和」関数は次のように表すことができます。
let sumsqr x = sum (sqr x)
そして次のように使用します:
> sumsqr [3;4;5]
50
または、xをパイプで通して定義することもできます。
let sumsqr x = x |> sqr |> sum
このように書かれているのは、xが一連の関数を介して「スレッド化」されるためだけに渡されていることは明らかです。直接構成の方がはるかに見栄えがします。
let sumsqr = sqr >> sum
これはより簡潔であり、私たちが行っていることについての別の考え方です。引数が流れるプロセスを想像するのではなく、関数を作成します。仕組みについては説明していませんsumsqr
。それが何であるかを説明しています。
PS:作曲に頭を悩ませる興味深い方法は、Forth、Joy、Factorなどの連結言語でプログラミングを試すことです。これらは: sumsqr sqr sum ;
、単語間のスペースが合成演算子。
PPS:おそらく他の人がパフォーマンスの違いについてコメントするかもしれません。パイプライン処理のように中間値を生成する必要がないことをコンパイラーに明確にすることで、構成によってGC圧力が低下する可能性があるように思われます。いわゆる「森林破壊」問題をより扱いやすくするのを助けます。
私はポイントフリーの概念に惹かれ、それをいくつかのことに使用し、前に述べたすべての肯定的なことに同意しますが、私はこれらのことを否定的なものとして見つけました(いくつかは上記で詳述されています):
表記が短いと冗長性が低下します。高度に構造化された構成(ramda.jsスタイル、Haskellのポイントフリー、またはその他の連結言語)では、コードの読み取りは、一連の
const
バインディングを線形にスキャンし、シンボル蛍光ペンを使用してどのバインディングが他のどのバインディングに入るかを確認するよりも複雑です。ダウンストリーム計算。ツリーと線形構造に加えて、説明的なシンボル名が失われると、関数を直感的に把握するのが難しくなります。もちろん、ツリー構造と名前付きバインディングの喪失の両方にも多くの利点があります。たとえば、関数はより一般的に感じられます-選択されたシンボル名を介して一部のアプリケーションドメインにバインドされていません-そしてツリー構造は意味的にも存在しますバインディングがレイアウトされていて、順番に理解できる場合(lisp let / let *スタイル)。ポイントフリーは、一連の関数をパイプでつなぐか構成するだけの場合に最も簡単です。これにより、人間が簡単に追跡できる線形構造が得られるためです。ただし、いくつかの中間計算を複数の受信者に通すのは面倒です。タプルへのあらゆる種類のラッピングがあり、レンズ効果やその他の骨の折れるメカニズムは、計算にアクセスできるようにするだけです。そうでなければ、値のバインドを複数回使用することになります。もちろん、繰り返される部分は別の関数として抽出できます。とにかくそれは良い考えですが、いくつかの非短い関数の引数もあり、抽出されたとしても、その引数は何らかの方法で両方のアプリケーションに通される必要があります。次に、実際に計算を繰り返さないように関数をメモ化する必要がある場合があります。1つは多くを使用します
converge
、、、など。lens
_memoize
useWidth
JavaScript固有:何気なくデバッグするのは難しい。バインディングの線形フローを使用
let
すると、どこにでもブレークポイントを簡単に追加できます。ポイントフリースタイルでは、ブレークポイントを何らかの方法で追加しても、値の流れが読みにくくなります。開発コンソールで変数をクエリしたり、カーソルを合わせたりすることはできません。また、ポイントフリーはJSにネイティブではないため、ramda.jsなどのライブラリ関数は、特にカリー化が義務付けられている場合、スタックをかなり覆い隠します。特に重要なサイズのシステムや本番環境でのコードの脆弱性。新しい要件が発生した場合、上記の欠点が発生します(たとえば、数週間後に自分自身になる可能性のある次のメンテナのコードを読むのが難しくなり、検査のためにデータフローを追跡するのも難しくなります)。しかし、最も重要なことは、一見小さくて無垢な新しい要件でさえ、コードのまったく異なる構造化を必要とする可能性があることです。新しいものを非常に明確に表現できるという点で良いことだと主張されるかもしれませんが、ポイントフリーコードの大規模なスワスを書き直すには非常に時間がかかるため、テストについては触れませんでした。したがって、より緩く、構造化されていない、字句割り当てベースのコーディングは、より迅速に再利用できると感じています。特にコーディングが探索的である場合、
ポイントフリーのバリアントである連結プログラミング言語に対して、私は次のように書かなければなりません。
私はJoyを少し経験しました。喜びは、リストを備えた非常にシンプルで美しいコンセプトです。問題をJoy関数に変換するときは、脳をスタック配管作業用の部分とJoy構文の解決用の部分に分割する必要があります。スタックは常に後ろから処理されます。コンポジションはJoyに含まれているため、コンポジションコンバイナーの計算時間はありません。