
「ビジュアルプログラミング」のために作成したWebエディターとその歴史についてお話します。
私たちには、ロシア郵便局との請求を扱う部署があります。 男は、紛失/未払い/未配達のアイテムを探しています。 開発者の要点であるこのタスクは、PostgreSQL方言で非常に多くの多様で巨大な(150行を超える)SQLクエリを書くことに要約されました。これは、新しい仮説の積極的な出現と注文に関する新しい情報の到着により、非常に頻繁に変更および補足されました。
当然のことながら、プログラマとして、私たちはこのビジネスにすぐに飽きてしまい、すぐにSQLに変換されるフィルターを作成するための視覚的なツールをすぐに提供したいと考えました。 最初は、多くの金型に大量の「入力」を付けたいだけでしたが、すぐに失敗しました。 すべてのフィルターが何らかの形で「構成」(構成)できる必要があることは明らかでした。グループに結合し、異なる条件と交差し、AND、OR、パラメーター化などを行います。 そして、実際に使用されている最も単純なフィルターでさえひどく見え始めました。 さらに、原則として、これらのフィルターは一般的なSQLコレクターのプライベートバージョンであり、一般に、「ビジュアルプログラミング」になるため、サブジェクトエリアを完全に無視することができます。
そして、映画「Flash of Genius」のように、キャラクターが敵に火の玉を発射したUE4(Unreal Engine 4)の設計図のビジュアルエディターの写真を見ました。

その夜に家に着いたとき、私は美しい長方形と複雑な線を描くことができる最初のJavaScriptライブラリを取りました-それは私たちの同胞DmitryBaranovskiyの Raphaëlであることが判明しました。 いくつかの長方形を描画し、ライブラリのドラッグアンドドロップを使用してそれらをぴくぴくさせた後、すぐにライブラリの作成者に、彼がそれをサポートしているかどうかの質問を書いた。 (今のところ)答えを待たずに、その夜に1000行以上のJavaScriptコードを作成しました。私の夢はほとんど目の前で実現しました。 しかし、まだ多くの仕事がありました。
最後に、私がやりたかったこと:
- さまざまなタイプの「ノード」を操作およびリンクするためのツールを提供するWeb上の美しく便利なエディター。初期化中にエディターに渡されるドメインスキームで説明されています。 したがって、エディターをサブジェクト領域から独立させます。
- ユーザーの非循環グラフをツリー構造にシリアル化する機能。JSONのように非常に簡単に解析し、あらゆる言語で解釈できます。
- サブジェクト領域のタイプとコンポーネントを記述するための便利な形式を提供します。
- ユーザーがサブジェクト領域の観点から正しいグラフを作成するのに役立つタイプと制限の豊富なシステムを考え出すこと。
- ユーザーが既存の組み合わせから独自のコンポーネントを作成できるようにします。
最後に、これは何が起こったかです:

ブループリントの図面は、ドメインスキーマに記述されているコンポーネントの具体的なインスタンスであるノードで構成されていることがわかります。 ノードはワイヤで接続されています。 ワイヤは常に、1つのノードの出力から別のノードの入力に(およびその逆に)進みます。 1つの出力から多くのワイヤを接続できますが、入力に接続できるのは1本だけです。
すべての入力と出力はタイプであり、したがって、可能な通信に制限を課します。 たとえば、次の型システムを考えます。
types: # Any, - Scalar: Numeric: extends: Scalar String: extends: Scalar List: typeParams: [A]
したがって、 数値型の出力をスカラー型の入力に関連付けることができますが、その逆はできません。 Listのようなパラメーター化された型の場合、共分散が暗示されます。 リスト[文字列]はリスト[スカラー]に渡すことができますが、その逆はできません。 さらに、常に他のすべての型に継承されるスーパー型Anyがあります。
ノードには、ワイヤを使用して構成されていない属性もあり、ユーザー自身が値を入力します。 これを行うには、valuePickersの概念があります。この概念を使用して、属性値を入力するための独自のインターフェイスを設定できます。 すぐに使用できるのは、テキスト入力と、定義済みの定数セットから選択する機能のみです。
ノードはタイプ別にパラメーター化されます。 たとえば、コンポーネントが与えられた場合:
IfThenElse: typeParams: [T] in: C: Boolean out: onTrue: @T onFalse: @T
IfThenElseコンポーネントに基づいてノードを作成する場合、エディターはタイプTを指定し、すべての場所でTに置き換えるように求めます。

入力および出力のタイプは、ユーザーが設計する際にも役立ちます。 数値タイプを使用して出力から投稿をプルし、マウスを放すと、入力が数値タイプと互換性がある(準拠する)コンポーネントのみが表示されるようにフィルターで除外されたコンポーネントを作成するためのウィンドウが表示されます。 また、自動投稿も添付されます。
これはすべて、きれいな3週間で行われ、5〜6か月間延長されました。 そして6ヵ月後、軍は何かを記録し、これを世界に知らせるように見えました。
紳士、作成する時が来ました! 「非技術的な」ユーザーに、数字を追加するプロセスを視覚的にプログラムする機能を提供する必要がある場合、最も非現実的なケースを取り上げましょう。 必要なのは、1種類の数値と2、3のコンポーネントのみです。 数値を指定する機能( リテラル )と、2つの数値を追加する機能( プラス )です。 以下は、このサブジェクト領域の回路の例です(回路のすべての例は、明確にするためにYAML形式で説明されていますが、実際には、ネイティブjavascriptオブジェクトを転送する必要があります)。
types: # Any, - Numeric: color: "#fff" components: Literal: # attrs: # V: Numeric out: # O: Numeric Plus: in: # A: Numeric B: Numeric out: O: Numeric
このスキームと簡単なグラフで組み立てられたエディタの例はここで見つけることができます 。
楽しんでください! Xを押して新しいアイテムを作成し、ダブルクリックしてアイテムを削除します。 ノードをワイヤで接続し、それらをすべて選択して、 Ctrl + CおよびCtrl + Vでコピーして貼り付けます。 次に、すべてのCtrl + Aを選択し、Deleteを使用して削除します。 結局、 Ctrl + Zを使用して 、いつでも元に戻すことができます!
ここで、単純なユーザーが次のグラフをコンパイルしたとしましょう。

エディターにグラフをツリーに保存するように依頼すると、次のようになります。
[ { "id": 8, "c": "Plus", "links": { "A": { "id": 2, "c": "Literal", "a": { "V": "2" }, "links": {}, "out": "O" }, "B": { "id": 5, "c": "Literal", "a": { "V": "2" }, "links": {}, "out": "O" } } } ]
ご覧のように、再帰的に歩き回って結果を得るのが非常に簡単なツリーがここに来ました。 バックエンドの言語もJavaScriptであるとしましょう(ただし、どの言語でもかまいません)。
簡単なコードを書きます:
function walk(node) { switch (node.c) { case 'Literal': return parseFloat(node.aV); case 'Plus': return walk(node.links.A) + walk(node.links.B); default: throw new Error("Unsupported node component: " + node.component); } } walk(tree);
このような関数を上記のツリーに沿って歩くと、2 + 2 = 4になります。 出来上がり!
非常に素晴らしいボーナスは、ユーザーが既存のコンポーネントを組み合わせて「機能」を定義できることです。
たとえば、単純に数字を追加できるような小さなドメインエリアがある場合でも、ユーザーはコンポーネントを特定できます。これにより、指定された数字に3が乗算されます。

これでカスタム関数x3ができました :

新しいコンポーネントとして使用できます:

同時に、すべてのユーザー関数がインライン化されているバックエンドにツリーを送信できます。開発者は、一部のノードがユーザー関数の一部であることさえ知りません。 ユーザー自身が、十分な想像力と忍耐力でデザイン言語を豊かにできることがわかりました。
折り畳み式の数値の最初のプリミティブな例に戻りましょう。 プロセッサは、追加することだけを行う統合デバイスであるという事実にもかかわらず、数字の追加レベルでエンドユーザーを視覚的にプログラムすることはあまり楽しくありません。 もっと表情豊かで豊かなデザインが必要です!
素晴らしいSQL言語を例にとってみましょう。 よく見ると、SQLクエリは実際には非常に簡単にツリーに分解されます(これは、データベースがクエリを受信したときに最初に行うことです)。 十分なタイプとコンポーネントを作成すると、 さらに威圧的になります。

PS例の1つが開かない場合...
おそらく、これはスキームの1つに対してユーザー定義関数を既に保存しようとしているという事実によるものです。 また、デフォルトでは(ただし、ハンドラーを定義することはできますが)すべてのユーザー関数はlocalStorageに格納されるため、エディターが現在のスキームに記述されていないコンポーネントまたはタイプをロードしようとすると状況が発生する場合があります。
これを行うには、次のコマンドで現在のlocalStorageをクリアします 。
これを行うには、次のコマンドで現在のlocalStorageをクリアします 。
localStorage.clear()
残念ながら、このグラフを実際のSQLに変換するデモは失敗します。なぜなら、 彼女は私たちのプロジェクトに強く結びついています。 しかし、実際には、これは次のSQLになります。
SELECT COUNT(o.id) AS cnt , (o.created_at)::DATE AS "" FROM tbl_order AS o WHERE o.created_at BETWEEN '2017-1-1'::DATE AND CURRENT_DATE GROUP BY (o.created_at)::DATE HAVING ( COUNT(o.id) ) > ( 100 ) ORDER BY (o.created_at)::DATE ASC
すぐに実行され、完成したレポートがExcel形式で提供されます。 このSQLを人間に転送すると、次のようになります。
2017年1月1日から開始し、100件未満の注文が読み込まれた日を除外して、1日あたりの読み込まれた注文の数を時系列順に表示します。 非常にリアルなビジネスレポートです!
この例のJSON形式の図は、 ここにあります 。
型システムとコンポーネントの詳細な説明はこの記事では行いません。 そのため、ドキュメントの適切なセクションに送信します 。 しかし、興味を温めるために、私はあなたがこのように書くことができるものを「投げ込む」だけです(ちょっとしたYAML構文を少し強調します):
Plus: typeParams: [T] typeBounds: {T: {<: Expr}} # 'T' 'Expr', # , 'Expr' in: A: @T B: @T out: O: @T
Scalaで関数を宣言したようです。
def Plus[T <: Expr](A: T, B: T): T = A + B
その結果、十分な数のコンポーネントを準備し、適切な型システムを思いついた(そして、ツリーを走査するため
最も重要なことは、MITライセンスの下でこのツールをGitHubのパブリックドメインと共有できることです。
誰も気にしない、ツールのさらなる開発のためのアイデア/タスクがあります:
- 回路内のコンポーネントのより便利なナビゲーション+ユーザー向けのドキュメント
- 属性ソケット(UE4の場合)
- カスタム関数で属性を定義する機能。
- 回路を表示するための読み取り専用モード
- 長方形だけでなく、カスタム形状ノード(UE4のような)
- 多数の要素を操作するための加速と最適化
- 国際化
- 画像をSVG / PNGにエクスポート
- あなたはそれに名前を付けます!
誰かが実際にこのツールを使用して、経験を共有したい場合はクールです。
PS誰かがツールの開発に参加すれば、さらにクールになります!
PPS このドキュメントを使用して、社内でツールのプレゼンテーションを行いました。