Javaでシステムを表すためのアプローチとしての矢印

多くの場合、構造図の形でシステム、アルゴリズム、およびプロセスの説明があります。 そのため、プログラミング言語で、例えば技術文書または仕様書から構造図を提示する緊急のタスク。


この記事では、John Hughes によって記述され、HaskellがYampaおよびNetwire FRPフレームワークとHaskell XML Toolbox XMLフレームワークで使用する矢印の概念を使用して、構造図を表現する方法について説明し ます


構造スキームの特徴は、処理されたデータ(変数)自体とその状態に焦点を合わせずに、一連の操作(ブロック)を視覚的に表現することです。 たとえば、ダイレクトゲインラジオを考えます


受信機ブロック図


既存の主流のプログラミング言語のフレームワーク内でシステムと計算を記述するこの方法を実装する方法は?


Cのようなプログラミング言語でのこのような回路の伝統的な記述は、次のようになります


//    Antenna antenna = new Antenna(Ether.getInstance()); Filter filter1 = new Filter(5000); //  -   Filter filter2 = new Filter(5000); Filter filter3 = new Filter(5000); Detector detector = new Detector("AM"); //   -  Amplifier amp = new Amplifier(5); //   Speaker speaker = new Speaker(10); //  Signal inputSignal = antenna.receive(); //     Signal filter1Res = filter1.filter(inputSignal); Signal filter2Res = filter2.filter(filter1Res); Signal filter3Res = filter3.filter(filter2Res); Signal detected = detector.detect(filter3Res); Signal amplified = amp.amplify(detected); speaker.speak(amplified); 

プログラムは、構造図にない変数を追加したため、ラジオ受信機の動作に関する情報は追加されず、入力信号の処理の中間結果を保存するためにのみ必要であることがわかります。


処理ユニットを通る信号の単純な段階的な通過のみを記述するこのようなプログラムは、やや面倒に見えます。 より簡潔なアプローチは、ブロックを互いに接続する新しいjoin方法を使用してスキームを記述することです。


 Receiver receiver = Receiver.join(filter1).join(filter2).join(filter3) .join(detector).join(amp).join(speaker); receiver.apply(antenna.receive()); 

join()メソッドは、ブロックの直列接続を記述します。つまり、 a.join(b)は、ブロックaによる処理の結果がブロックb入力に転送されることを意味します。 この場合、 FilterAmplifierDetectorSpeaker接続されたクラスは、 apply()メソッドを追加実装するだけでよく、「default action」(Filter Filterfilter()Amplifieramplify()など)を実行し、オブジェクトを関数として呼び出すことができます。


機能的なアプローチでは、これらのクラスは関数を返す関数になるため、クラスをインスタンス化する必要さえなく、プログラム全体は次のようになります。


 Receiver receiver = Receiver.join(filter(5000)).join(filter(5000)).join(filter(5000)) .join(detector("AM")).join(amplifier(5)).join(speaker(10)); receiver.apply(antenna.receive()); 

計算を説明する方法としての矢印


関数型アプローチの特徴は、コンビネータ(たとえば、モナド)の使用です。これは、他の関数を複合計算に結合する関数です。


矢印も組み合わせ子であり、複合計算の一般的な説明を可能にします。 この記事では、Java 8で記述されたjArrows arrow実装を使用します。


矢印とは何ですか?


関数Out f(In x)の矢印Arrow<In, Out> aは、関数Out f(In x)によって実行される計算を表します。 ご想像のとおり、 Inは矢印の入力値のタイプ(関数f受け入れられる)、 Outは出力値のタイプ(関数f返される)です。 計算を矢印の形で表す利点は、さまざまな方法で計算を明示的に組み合わせることができることです。


例えば、Javaでのy = x * 5.0の計算は、関数によって表されます


 double multBy5_0(int in) { return in*5.0; } 

矢印として表すことができます


 Arrow<Integer, Double> arrMultBy5_0 = Action.of(multBy5_0); 

矢印でパックされた計算は、他の矢印の計算と組み合わせることができます。 ActionクラスはArrowインターフェースの実装の1つです。 このインターフェイスの別の実装は、マルチスレッドコンピューティングをサポートするParallelActionです。


矢印の構成


矢印arrMultBy5_0は別の矢印と直列に接続できます。たとえば、入力値に10.5を追加してから、次の矢印で結果を文字列として表します。 矢印のチェーンを取得する


 Arrow<Integer, String> mult5Plus10toStr = arrMultBy5_0.join(in -> in+10.5) .join(in -> String.valueOf(in)); mult5Plus10toStr.apply(10); // "60.5" 

合成矢印mult5Plus10toStrで表される計算結果は、ブロック線図として表すことができます。



この矢印の入力はInteger型(チェーンの最初の計算の入力型)であり、出力はString (チェーンの最後の計算の出力型)です。


someArrow.join(g)メソッドは、 someArrow矢印で表される計算をgで表される計算とsomeArrowます。一方、 gは別の矢印、ラムダ関数、メソッド、またはapply(x)メソッドでApplicableインターフェースを実装する何かapply(x) 、入力値x適用できます。


結合のやや単純化された実装
 class Action<In, Out> implements Arrow<In, Out>, Applicable<In, Out> { Applicable<In, Out> func; public Arrow<In, OutB> join(Applicable<Out, OutB> b) { return Action.of(i -> b.apply(this.func.apply(i))); } } 

ここで、 Inは矢印aの入力データ型、 OutBは出力データ型b 、結果の新しい複合矢印a_b = a.join(b)の出力型、 Outは矢印a出力データ型、および入力データ型です。矢印b


func関数は、矢印インスタンスに格納され、作成時に初期化され、計算自体を実行します。 引数bApplicableインターフェースをサポートし、異なる矢印または関数である可能性があるため、 a.func(i)を矢印a_b入力i適用した結果にbを適用します。 複合矢印a_b apply呼び出しa_b呼び出されると、入力データ自体が転送されるため、 a_b.apply(x)は計算b.func(a.func(x))の結果を返します。


その他の矢印の合成方法


joinメソッドを使用したシリアル接続に加えて、 combinecloneInput 、およびsplitメソッドを使用して矢印を並列に接続できます。 combine方法を使用して、 sin(x)^2+cos(x)^2の計算を記述する例


 Arrow<Pair<Double, Double>, Pair<Double, Double>> sin_cos = Action.of(Math::sin).combine(Math::cos); Arrow<Double, Double> sqr = Action.of(i -> i*i); Arrow<Pair<Double, Double>, Double> sum_SinCos = sin_cos.join(sqr.combine(sqr)) .join(p -> p.left + p.right); sum_SinCos.apply(Pair.of(0.7, 0.2)); // 1.38 


結果の「幅の広い」矢印sin_cosは、タイプPair<Double, Double>の値のペアを受け取ります。 Pair<Double, Double>の最初のpair.left値は、最初の矢印の入力(sin関数)、2番目のpair.right -2番目の矢印の入力(cos関数)、結果もペアになります。 次の合成矢印sqr.combine(sqr)は、タイプPair<Double, Double>値を受け入れ、 Pair<Double, Double>両方の値を二乗します。 最後の矢印は結果をまとめたものです。


someArrow.cloneInput(f)メソッドは、矢印を作成し、 someArrowfを並列に接続して入力に適用します。その出力は、これらの矢印の計算結果を組み合わせたペアとして表示されます。 入力タイプsomeArrowfは一致する必要があります。


 Arrow<Integer, Pair<Integer, Double>> sqrAndSqrt = Action.of((Integer i) -> i*i) .cloneInput(Math::sqrt); sqrAndSqrt.apply(5); // Pair(25, 2.236) 


この場合、並列接続とは、並列に接続された2つの計算の結果が互いに独立していることを意味します。一方の計算の結果が他方の入力に転送されるjoin方法による直列接続とは異なります。 マルチスレッドの並列接続は、 ParallelActionクラスによって実装されます。


someArrow.split(f, g)メソッドは、 someArrow.join(f.cloneInput(g))と同等の追加メソッドです。 someArrowの計算結果は入力fgに同時に送信され、そのような矢印の出力は計算fg結果とのペアになります。


バイパスの計算


場合によっては、計算結果とともに矢印の入力値の一部をチェーンに沿って転送する必要があります。 これはsomeArrow.first()メソッドによって実装され、 someArrow.first()で補完してsomeArrow.second()矢印を変換し、結果の矢印が入力として値のペアを取り、このペアの要素の1つだけに計算を適用するようにします


 Arrow<Integer, Double> arr = Action.of(i -> Math.sqrt(i*i*i)); Pair input = Pair.of(10, 10); arr.<Integer>first().apply(Pair.of(10, 10))); // Pair(31.623, 10) arr.<Integer>second().apply(Pair.of(10, 10))); // Pair(10, 31.623) 



これらのメソッドはsomeArrow.bypass1st()それぞれsomeArrow.bypass2nd()およびsomeArrow.bypass1st()似ています。


説明の完全性


ヒューズによると、次の3つの関数のみを使用して計算を行うことができます。



jArrows実装も、システムの説明を簡素化する追加のメソッドで拡張されています。


結論


フローチャートの形でのプロセスの高レベルの説明は、プログラミングへの命令型アプローチでは実際には使用されません。 同時に、このような記述は、より広く普及しつつある機能的なリアクティブアプローチにうまく適合します。


ヒューズの記事に示されているように、基本的に関数型プログラミングのフレームワーク内のフローチャート形式のシステム記述の実装である矢印は、主流、特に実装の形ですでに普及しているモナドよりも一般化された記述です。この記事では、このアプローチの基本原則について説明します。今後、主流のソフトウェア開発で矢印を使用するために既存のパターンを適用し、新しいパターンを開発することが重要です。



Source: https://habr.com/ru/post/J311914/


All Articles