スタックプログラミング言語

連結的

関数型プログラミングが復活しました。 クラシックを好むかハードコアを好むか、課された業界標準に苦しむか、単に流行に敏感かによって、Scala、Haskell、F#、または古き良きLispが好きかもしれません。 高次の関数としての単語の組み合わせ、副作用がないこと、さらにはモナドでさえ、JetBrainsから来た人であろうとSICPを初めて見た学生であろうと、すべての「壊れやすい若い心」の耳を愛careします。



しかし、文字通りの意味でさらに機能的な別のプログラミングがあり、基本的にはラムダ計算ではなく、関数の構成を持ちます。 そして、彼について少しお話ししたいと思います。



連結プログラミングとは何ですか?





連結は、複数の行の組み合わせです。 2つのコードフラグメントの通常の組み合わせが結果として合成される言語は、連結[1]と呼ばれます。 これらの各コードは、スタックを引数として受け取り、その結果としてスタックを返す関数です。 したがって、より頻繁にそのような言語はスタックベースと呼ばれます。



スタック言語は生きており、存在しています。 翻訳者のスピードと軽さにより、実際の世界で使用されています。 JVM、おそらく世界で最も普及している仮想マシン。 CPython、別のスタック仮想マシン[2]。YoutubeやBitTorrentなど、多くの負荷の高いプロジェクトで使用されます。 現在、ハイエンドのハイエンドプリンターで使用されているPostScriptで、最後に、古き良きForthがマイクロコントローラーや組み込みシステムのプログラミングで使用されています。 それらはすべて独自の方法で素晴らしいですが、一般的な原則は似ていますが、概して、Explorer、Joy、Catなどの高レベル言語について説明することに注意してください。



機能





連結言語では、すべてが関数ですが、他の言語と比較するために、定義付きの二乗関数の例を見てみましょう。 揺るぎない命令型C



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





機能スキーム



 (define square (lambda (x) (* xx)))
      
      





連結ジョイ



 DEFINE square == dup * .
      
      







連結因子

 : square ( x -- y ) dup *;
      
      







連結

 define square { dup * }
      
      







ここで、 dupは引数のコピーをスタックの一番上に作成し、そのコピーを同じ場所に置きます。

*一般的な乗算関数です
 *:: (int) → (int) → (int)
      
      



スタック上の2つの引数を「予期」します。 この場合、スタックの最後の2つは同じ番号のコピーです。 この関数を使用したい場合、 3 squareを書くだけで十分です。ここで、3は署名付きの関数でもあります
 3::() → (int)
      
      



、つまり 彼女自身を返します。 実際、行ポリモーフィズムを持つタイプであり、現在のスタック全体を返すと言う方が正しいです[5]



連結言語では、変数とラムダは使用されません(より正確には推奨されませんが、本当に必要な場合)。この事実により、かなり簡潔になります。 適用可能なアプローチの代わりに、スタックで作業するデータのシーケンスに対応するポストフィックス(逆ポーランド語)表記で記録されたコンポジションがあります。これは、スタックコードを読みやすくすることを意味します。 2つの例を見てみましょう。 まず、これは通常のHelloの世界であり、この表記ではやや異常に見えます。



 "Hello world" print
      
      







文字列「Hello World」をスタックに配置し、次にprint関数を使用してスタックの最上部から要素を抽出し、画面に表示します。



 10 [ "Hello, " "Factor" append print ] times
      
      







後で説明する引用符の操作を使用しますが、10と引用符がスタックに置かれ、timesが指定された回数だけ引用符からコードを実行することは非常に明白です。 引用符内のコードは、スタック上の2行を連続してプッシュし、それらを連結して表示します。



trybrief.comのブラウザーで、小さな連結言語を直接試すことができます。 簡単にわかるように、インタープリターはjavascriptで記述されており、非常にシンプルです; ./Engine.jsで表示できます。 インタプリタはもともとC#で実装され、現在は他の多くの言語で実装されているCatにも、 オンラインjsインタプリタがあります。



引用符とコンビネータ。





コードの一部を引用符([])で囲む機能により、独自の制御構造を作成することができます。 引用は、引用された式の内容を返す関数です。 この場合の引用関数は次のタイプになります。



 quote :: (T) → (() → T).
      
      







たとえば、角括弧内のエントリ[5]は、整数5自体ではなく、引用符を返します。 式については、たとえば[5 6 +]のように明確です。 引用符を付けない場合、数値11が計算されてスタックに配置されますが、引用符を付けると式5 6 +がスタックに配置され、さまざまな方法で対話できます。

コンビネータは、スタックから1つ以上の引用を予期し、スタックの残りの部分に特別な方法でそれらを適用する関数です。



引用は基本的に定期的なリストです。 他の引用符と組み合わせて、スタックでさまざまな操作を実行し、異なるコンビネーターを使用して適用できます。 たとえば、foldコンビネータはリストを値に折りたたみます。リストアイテムを合計する例(Joy):



 [2 5 3] 0 [+] fold
      
      







Joyのbinrec再帰コンビネーターを使用したクイックソート:



 DEFINE qsort == [small] [] [uncons [>] split] [[swap] dip cons concat] binrec .
      
      







Factorのようにモナドの実装を妨げるものはありません。たとえば、末尾再帰を最適化できます。



制御構造。





引用とコンビネータのおかげで、現在のものを使用したり、独自の制御構造を作成したりできます。 if(要素)の例:



 = [ 23 ] [ 17 ] if
      
      







これがどのように機能するかを理解するために、ラムダ計算のブール関数定義をブラッシュアップすることをお勧めします。 Factorのもう少し複雑な例:



 : sign-test ( n -- ) dup 0 < [ drop "" ] [ zero? [ "" ] [ "" ] if ] if print ;
      
      







この関数は、数値の符号を決定します。 ifが2つの引用符で動作し、角括弧で強調表示され、式dup 0 <より上のスタックに配置されます。 sign-testを呼び出すときにスタックの一番上にある数値(つまり、関数の引数)がゼロより小さい場合、最初の引用符が適用され、「負」がスタックにプッシュされます(そこからコピーされたチェック済みの数値が破棄されます)。 数値が大きい場合、2つ目の引用が実行され、内部にもう1つの条件が含まれます。 その後、スタック上に数字の符号を示す行があり、この行は印刷機能によって印刷されます。



利点:





スピードと最適化



  :A 1 2 + print :add2 2 + ; :B a add2 print
      
      







この場合、コンポジションの結合性により、A = Bです。 連結言語のプログラムは、コンパイル後に収集される部分に分割されます。 基本的に、これは通常のmap-reduceであり 、高速であり、各コードを並行してコンパイルできます。

リファクタリング コードの2つの同じセクションが表示される場合は、それらを別の関数に取り出します。 コンビネータ、引用、および独自の制御構造を使用します。 猫の最適化の例:



 f map g map => [fg] map [f] map x [g] fold => x [[f' f] g] fold [f] filter x [g] fold => x [f [g] [pop] if] fold [f] map [g] filter x [h] fold => x [fg [h] [pop] if] fold
      
      







難しさ





スタック言語のすべての欠点は、明らかにその機能に起因しています。 これはアプリケーションではなく構成であり、意味のない表記法であり、変数がなく、いくつかの式を書くのが難しいことです。 連結言語'abc d'の簡単な表現を想像してください。 関数cが2つの引数を取る場合、適用可能な形式では、式はd(c(b、a))として記述されます。bcが1つの引数を取る場合、 d(c(b(a))) ; dが 2つの引数の関数、 bが 1、 cが引数を受け取らない関数の場合、適用可能な形式では式はd(c、b(a))のようになります。 コードのすべてを完全に理解する必要があるため、コードを連結言語で非常に慎重に記述する必要があります。 これは、Catでの厳密な型指定、IDEからのサポートの申し立て、および考えを変えることができるさまざまな方法とメカニズムによってサポートされています。 しかし、このような問題は、機能構成(ドットが存在する)の無知な表記法で、最愛のHaskellであなたを追い越すことができます。 名前付き変数を持たない接尾辞形式で複雑な代数式を記述することも、子供の頃から使用していたのと同じ方法では不可能です。 これがこれらの言語の主な問題です-それらは、古典的な機能言語と同様に、あなたとは少し異なる考え方を必要とすることです。 生産のコンテキストでは、Scalaのストーリーはまだ落ち着いていませんが、すべてが人的資源にかかっているため、誰もが使い慣れた言語が必要です。



連結言語の各関数はスタックで動作し、それ自体に副作用がない場合もありますが、割り当てや変数はありませんが、スタックを変更します。 そのため、多くの人にとって清潔の問題は明らかではありません。 ただし、問題の高水準言語では、これは実装の問題であるため、完全に真実ではありません。 そして、現実の世界に適用するために、それらは必要なまれなケースのための変数も持っています(たとえば、SYMBOL:ファクターまたは関数の宣言でコードの可読性を高める)、それらは数学的に非常に純粋です(Joy [ 4] )そして、アプローチ、パラダイム、プラットフォーム間の多くの概念的な違いのように、最終的には単なる好みの問題です。 タスクに適したツールの選択を提供しました。 そのため、Forthは組み込みシステムで使用され、そのトランスレータは実装が非常に簡単で、非常に小さくなります。



誰かが新しい何かを発見したことを願っています。 個人的には、「なぜ連結プログラミングが重要なのか」 [5]というテキストに触発され、数日間、このトピックから離れることができず、眠い状態にあるときでも何度も「連結」という言葉を言うことができます。



1. ウィキペディア:連結プログラミング



2. Pypyインタープリター



3. http://docs.factorcode.org/content/article-cookbook-philosophy.html



4. JoyおよびKでの関数型プログラミング



5. 連結プログラミングが重要な理由



6. オブジェクトとアスペクト:行ポリモーフィズム



All Articles