Rx.jsを使用してモールス信号を認識する









タスク:キーボードからの入力信号(キーアップ、キーダウン)-モールス信号でデコードされた出力文字と単語。 FRPアプローチ、特にRx.jsを使用してこの問題を宣言的に解決する方法については、以下をご覧ください。 (なぜ?できるから)





非患者の場合:



主なアイデア



このデモは、非同期コンピューティングの構成の問題を解決するためのJediリアクティブプログラミングの能力を実証することを目的としています。 Rxは、イベントをコレクションとして扱い、非同期コードの見方を変えます。 Rxおよび関数型プログラミングに関する多くの記事は次のようになります。











したがって、アプリケーションの機能部分の1つを(ライブの例と図を使用して)詳細に説明しようとします。



モールス論理



モールス符号の文字は、特定の時間間隔で区切られた一連の長い信号と短い信号(ドットとダッシュ)です。



基本ルール(理想):

  1. 単位時間は1ポイントの期間です。
  2. ダッシュの長さは3ポイントです。
  3. 同じ符号の要素間の休止は1ポイントです。
  4. 単語内の文字間で一時停止-3ポイント。
  5. 単語間の一時停止-7ポイント。


将来については、ディメンションについてあまり気にせず、「目で」400ミリ秒のポイント期間を要したことを警告します。 このプロジェクトは、実際のモールス信号と100%互換性があると主張していません(軍事状況で使用しないでください)が、動作原理は同じままです。 比較的400ミリ秒、残りの時間間隔が計算されます(ダッシュサイズ、文字と単語の間の一時停止)。 このインターフェースは、文字、新しい文字、または次の単語から文字(ピリオド、ダッシュ)が必要なときに明確になるように構築されています。



-何? はい、私は5分で膝の上にいます、そしてRxなしでそれをします



主な難点は何ですか?



主な難点は、非同期ロジックを扱っていることです。 文字内の信号の数は非決定的です。 たとえば、文字「A」はドットとダッシュ(.-)の2文字で構成され、「0」は5つのダッシュ(-----)です。 また、ある文字から別の文字までの時間をカウントする方法を想像するのは簡単ではありません。 単語間に間隔があることをどのように理解することができますか?..この問題は、コールバックまたはプロミスとsetTimeoutまたはnew-fangled async / awaitの束を伴う標準的な命令型アプローチによって解決できます。 これが間違っていると納得させたくはありません。私が気に入った別のアプローチを示したいと思います。



問題の分解とさまざまな抽象化層



-分割とインペラ!!!



複雑な問題を解決するには、それをより小さくシンプルなサブタスクに分割し、それぞれを個別に解決する必要があります。 この場合、入力には低レベルの信号(DOMイベント)があり、出力には文字と単語があります。 このタスクは、 OSIネットワークモデルと比較できます。 モデルはさまざまなレベルで表され、それぞれが独自のタスクを実行して、上位層にデータを提供します。 主な類似点は、各レベルに独自の明確なロジックがありますが、モデル全体に​​ついては知らないことです。 タスクの抽象化の主要なレイヤーを強調しましょう。







ご覧のとおり、各レイヤーは独自のロジックで動作し、スタンディングレイヤーの上にデータを提供し、システム全体については知りません。



最初のクラスのオブジェクトとしてのイベントのシーケンス



Rxでは、非同期シーケンスを最初のクラスのオブジェクトと見なすことができます。 これは、発生するすべてのキーアップとキーダウンを特定のコレクション(Observable)に保存し、通常のデータ配列と同様に操作できることを意味します。



「ドットまたはダッシュ」タスクを分析しましょう



次に、ドットまたはダッシュが来るストリームを取得するプロセスを詳細に説明します。 開始するには、すべてのキーストロークのコレクションを取得します。



const keyUps = Rx.Observable.fromEvent(document, 'keyup'); const keyDowns = Rx.Observable.fromEvent(document, 'keydown');
      
      





jsfiddleの例



kyeupイベントとkeydownイベントの配列を受け取ったので、スペースバーを押すことにのみ興味があります。 フィルター操作を使用して取得できます-これは、DOMイベントの抽象化の最初のレベルになります。



 const spaceKeyUps = keyUps.filter((data) => data.keyCode === 32); const spaceKeyDowns = keyDowns.filter((data) => data.keyCode === 32);
      
      





jsfiddleの例



概略的には、次のようになります。



上の図では、2つのストリームが表示されています。 上部にはすべてのキーダウンイベントが含まれます。 下部のものは上部のものに基づいていますが、ご覧のように、フィルター機能が適用され、押されたキーのコードをフィルターします。 その結果、キーダウンスペースのある新しいストリームができました。 どこかでDOM APIのspaceKeyDownイベントを見ましたか? 既存のDOMイベントに基づいて作成したばかりで、さらに使用します。



信号の発信元(マウス、キーストローク、マイク、カメラ)には特に関心がありません。信号が開始または終了したという事実を抽象化して伝えます。



 const signalStarts = spaceKeyDowns.map(() => "start"); const signalEnds = spaceKeyUps.map(() => "end");
      
      





jsfiddleの例

しかし、すべてがsignalStartsでそれほど単純ではありません:)
keydownイベントには小さな問題があります。 DOM APIは、キーが押されるとkeydownイベントが何度も発生するように機能します。 いくつかのコードを追加することで、これを簡単に克服できます。



 const signalStartsRaw = spaceKeyDowns.map(() => "start"); const signalEndsRaw = spaceKeyUps.map(() => "end"); //     start  end. const signalStartsEnds = Rx.Observable.merge(signalStartsRaw, signalEndsRaw).distinctUntilChanged(); // signal star/end with toggle logic const signalStarts = signalStartsEnds.filter((ev) => ev === "start"); const signalEnds = signalStartsEnds.filter((ev) => ev === "end");
      
      





ここで何が起こったのか見てみましょう。 主な問題は、2つの同一の連続したイベントの発生です。 この場合、開始と終了(signalStartsEnds)から一般的なストリームを取得し、 distinctUntilChanged関数を適用できます。 彼女は、イベントが繰り返されないようにします。 (distinctUntilChangedの詳細はこちら



jsfiddleの動作例





次に、信号の開始から終了までの時間を計算する必要があります。このため、コレクションにタイムスタンプを追加しましょう。



 const signalStarts = signalStartsEnds.filter((ev) => ev === "start").timestamp(); const signalEnds = signalStartsEnds.filter((ev) => ev === "end").timestamp();
      
      





その後、キーダウンとキーアップの時間差を返す必要があります。 このために別のストリームを作成しましょう。 キーアップイベントの発生は確定的ではありません。 つまり、キーダウンをストリームと見なし、各キーストロークの時間を記録する場合、各イベントは最初のキーアップ値を返す別のストリームを返す必要があります。 それは非常に複雑に聞こえますが、コードでどのように見えるかを見るのは簡単です:



 const spanStream = signalStarts.flatMap((start) => { return signalEnds.map((end) => end.timestamp - start.timestamp).first(); });
      
      





jsfiddleの例



概略的には、次のようになります。





イメージt1、t2、t3では、これはイベントが発生した時間です。 t2-t1-時間差。 「信号の開始ごとに、信号の終了の信号からストリームを作成し、そこから最初の信号を待ってから、信号の開始と終了の時間差を転送します。」 したがって、時間間隔からストリームを取得し、それらからポイントとダッシュを決定できます。



 const SPAN = 400; const dotsStream = spanStream.filter((v) => v <= SPAN).map(() => "."); const lineStream = spanStream.filter((v) => v > SPAN).map(() => "-");
      
      





完全なサンプルコードは次のようになります-jsfiddleの例 。 ノイズをいくつか取り除いて、より美しいコードを取得しましょう(主観的な意見ですが、気に入らなければ魂はありません)。



そして、ここにドットとダッシュの約束された流れがあります:



 const dotsAndLines = Rx.Observable.merge(dotsStream, lineStream);
      
      





次に、より高いレベルのストリームを操作し、それらからさらに高いレベルのストリームを作成します。 たとえば、複数のRx変換を適用すると、文字と文字の間のスペースのストリームを取得できます。



 const letterCodes = dotsAndLines.buffer(letterWhitespaces) //   [['.', '.', '-'], ['-', '.', '-'] ... ] const lettersStream = letterCodes.map((codes) => morse.decode(codes.join(""))).share() //   ['A', 'B' ...]
      
      





ソースへのリンク

いいね? いや? 次に、ユーザーがコードに「CAT」という単語を入力した場合に猫の写真を表示する方法を示します。



 const setCatImgStream = wordsStream.filter((word) => word == "CAT").map(setCatImg)
      
      





証明




おわりに



したがって、通常のDOMイベントから、より意味のあるもの(文字と単語のあるストリーム)に到達したことがわかります。 将来的には、これをさらに文章、段落、書籍などにまとめることができます。主なアイデアは、Rxを使用して既存の機能を構成し、新しい機能を追加できるようにすることです。 すべてのロジックは、より複雑な新しいものを構築するための一種のAPIに変わり、それを順番に組み合わせることができます。 この記事では、Rxがすぐに使用できる多くの利点(非同期チェーンのテスト、リソースのクリーニング、エラーの処理)を逃しました。 私は試してみたかったがあえてしなかった人たちに興味を持てたことを願っています。



ご清聴ありがとうございました! すべてのFP =)



All Articles