宣言型プログラミング:それは本物ですか?
公開: 2022-03-11宣言型プログラミングは、現在、データベース、テンプレート、構成管理など、広範で多様なドメインのセットの主要なパラダイムです。
一言で言えば、宣言型プログラミングは、プログラムに実行方法を指示するのではなく、実行する必要があることをプログラムに指示することで構成されます。 実際には、このアプローチでは、ユーザーが望むものを表現するためのドメイン固有言語(DSL)を提供し、目的の最終状態を実現する低レベルの構成(ループ、条件、割り当て)からユーザーを保護する必要があります。
このパラダイムは、それが置き換えた命令型アプローチを大幅に改善したものですが、宣言型プログラミングには重大な制限があり、この記事で検討する制限があると私は主張します。 さらに、私は、その制限に取って代わりながら、宣言型プログラミングの利点を取り込む二重のアプローチを提案します。
警告:この記事は、宣言型ツールとの複数年にわたる個人的な闘いの結果として登場しました。 私がここに提示する主張の多くは完全に証明されておらず、いくつかは額面通りに提示されています。 宣言型プログラミングの適切な批評にはかなりの時間と労力がかかり、私は戻ってこれらのツールの多くを使用する必要があります。 私の心はそのような仕事ではありません。 この記事の目的は、あなたといくつかの考えを共有し、パンチを抜かず、私にとって何がうまくいったかを示すことです。 宣言型プログラミングツールに苦労している場合は、休息や代替手段が見つかるかもしれません。 そして、パラダイムとそのツールを楽しんでいるのなら、私をあまり真剣に受け止めないでください。
宣言型プログラミングがうまく機能するのであれば、私はそれ以外のことを言う立場にはありません。
宣言型プログラミングのメリット
宣言型プログラミングの限界を探る前に、そのメリットを理解する必要があります。
おそらく最も成功している宣言型プログラミングツールは、リレーショナルデータベース(RDB)です。 それは最初の宣言型ツールでさえあるかもしれません。 いずれにせよ、RDBは、宣言型プログラミングの典型であると私が考える2つの特性を示します。
- ドメイン固有言語(DSL) :リレーショナルデータベースのユニバーサルインターフェイスは、構造化クエリ言語という名前のDSLであり、最も一般的にはSQLとして知られています。
- DSLは、下位レベルのレイヤーをユーザーから隠します。RDBに関するEdgar F. Coddの元の論文以来、このモデルの力は、必要なクエリを、それらを実装する基になるループ、インデックス、およびアクセスパスから分離することであることは明らかです。
RDBが登場する前は、ほとんどのデータベースシステムは、レコードの順序、インデックス、データ自体への物理パスなどの低レベルの詳細に大きく依存する命令型コードを介してアクセスされていました。 これらの要素は時間の経過とともに変化するため、データの構造に根本的な変化があったために、コードが機能しなくなることがよくあります。 結果として得られるコードは、記述、デバッグ、読み取り、および保守が困難です。 私は手足を出して、このコードのほとんどは、おそらく、長い間、条件付きのことわざのネズミの巣、繰り返し、そして微妙な状態依存のバグでいっぱいだったと言います。
これに直面して、RDBはシステム開発者に大きな生産性の飛躍をもたらしました。 これで、数千行の命令型コードの代わりに、明確に定義されたデータスキームに加えて、数百(または数十)のクエリが作成されました。 その結果、アプリケーションは、データの抽象的で意味のある永続的な表現を処理し、強力でありながらシンプルなクエリ言語を介してデータをインターフェイスするだけで済みました。 RDBは、おそらくプログラマーとそれを採用した企業の生産性を桁違いに向上させました。
宣言型プログラミングの一般的にリストされている利点は何ですか?
- 読みやすさ/使いやすさ:DSLは通常、擬似コードよりも自然言語(英語など)に近いため、プログラマー以外の人でも読みやすく、習得しやすくなっています。
- 簡潔さ:ボイラープレートの多くはDSLによって抽象化され、同じ作業を行うための行が少なくなります。
- 再利用:さまざまな目的に使用できるコードを作成する方が簡単です。 命令型構造を使用する場合、悪名高いほど難しいことです。
- べき等:終了状態を操作して、プログラムにそれを理解させることができます。 たとえば、アップサート操作を使用すると、両方の場合に対処するコードを記述する代わりに、行が存在しない場合は行を挿入するか、すでに存在する場合は行を変更できます。
- エラー回復:考えられるすべてのエラーに対してエラーリスナーを追加する代わりに、最初のエラーで停止する構造を簡単に指定できます。 (node.jsで3つのネストされたコールバックを作成したことがある場合は、私が何を意味するかを知っています。)
- 参照透過性:この利点は一般に関数型プログラミングに関連していますが、実際には、状態の手動処理を最小限に抑え、副作用に依存するあらゆるアプローチに有効です。
- 可換性:実装される実際の順序を指定しなくても、終了状態を表現できる可能性。
上記はすべて宣言型プログラミングの一般的に引用されている利点ですが、私はそれらを2つの性質に凝縮したいと思います。これは、別のアプローチを提案する際の指針として役立ちます。
- 特定のドメインに合わせて調整された高レベルのレイヤー:宣言型プログラミングは、それが適用されるドメインの情報を使用して高レベルのレイヤーを作成します。 データベースを扱う場合、データを扱うための一連の操作が必要であることは明らかです。 上記の7つの利点のほとんどは、特定の問題ドメインに正確に合わせた高レベルのレイヤーを作成することから生じます。
- ポカヨケ(絶対確実) :ドメインに合わせた高レベルのレイヤーは、実装の必須の詳細を隠します。 これは、システムの低レベルの詳細にアクセスできないため、コミットするエラーがはるかに少ないことを意味します。 この制限により、コードから多くのクラスのエラーが排除されます。
宣言型プログラミングに関する2つの問題
次の2つのセクションでは、宣言型プログラミングの2つの主な問題である分離性と展開の欠如について説明します。 すべての批評にはそのブギーマンが必要なので、宣言型プログラミングの欠点の具体例としてHTMLテンプレートシステムを使用します。
DSLの問題:分離性
自明ではない数のビューを持つWebアプリケーションを作成する必要があると想像してください。 これらのページの多くのコンポーネントが変更されるため、これらのビューをHTMLファイルのセットにハードコーディングすることはできません。
文字列を連結してHTMLを生成するという最も簡単な解決策は、非常に恐ろしいように思われるため、すぐに別の方法を探すことになります。 標準的な解決策は、テンプレートシステムを使用することです。 テンプレートシステムにはさまざまな種類がありますが、この分析のためにそれらの違いを回避します。 テンプレートシステムの主な使命は、データレコードをループするコードの代替としてRDBが登場したように、条件とループを使用してHTML文字列を連結するコードの代替を提供することであるという点でそれらすべてが類似していると見なすことができます。
標準のテンプレートシステムを使用するとします。 3つの摩擦の原因に遭遇しますが、重要度の高い順にリストします。 1つ目は、テンプレートは必ずコードとは別のファイルに存在するということです。 テンプレートシステムはDSLを使用するため、構文が異なり、同じファイルに含めることはできません。 ファイル数が少ない単純なプロジェクトでは、個別のテンプレートファイルを保持する必要があるため、ファイルの量が重複または3倍になる可能性があります。
組み込みRubyテンプレート(ERB)は、Rubyソースコードに統合されているため、例外を開きます。 他の言語で記述されたERBに着想を得たツールの場合、これらのテンプレートも別のファイルとして保存する必要があるため、これは当てはまりません。
摩擦の2番目の原因は、DSLには独自の構文があり、プログラミング言語の構文とは異なることです。 したがって、DSLの変更(独自の書き込みは言うまでもなく)はかなり困難です。 内部に潜り込んでツールを変更するには、トークン化と解析について学ぶ必要があります。これは面白くてやりがいがありますが、難しいことです。 私はこれを不利だと思っています。
「いったいなぜツールを変更したいのですか? 標準的なプロジェクトを行っている場合は、適切に作成された標準的なツールが適切です。」 たぶんそうだけどたぶん違う。
DSLはプログラミング言語のフルパワーを持っていることは決してありません。 もしそうなら、それはもはやDSLではなく、完全なプログラミング言語になるでしょう。
しかし、それがDSLの要点ではありませんか? プログラミング言語のフルパワーを利用できないようにして、抽象化を実現し、バグのほとんどの原因を排除できるようにするには? 多分はい。 ただし、ほとんどのDSLは単純なものから始まり、実際には1つになるまで、プログラミング言語の機能を徐々に組み込んでいきます。 テンプレートシステムは完璧な例です。 テンプレートシステムの標準機能と、それらがプログラミング言語機能とどのように相関するかを見てみましょう。
- テンプレート内のテキストを置き換える:変数の置換。
- テンプレートの繰り返し:ループ。
- 条件が満たされない場合は、テンプレートの印刷を避けてください:条件付き。
- パーシャル:サブルーチン。
- ヘルパー:サブルーチン(パーシャルとの唯一の違いは、ヘルパーが基礎となるプログラミング言語にアクセスして、DSLストレートジャケットから抜け出すことができることです)。
DSLはプログラミング言語の力を同時に欲しがり、拒絶するために制限されるというこの議論は、DSLの機能がプログラミング言語の機能に直接マッピングできる範囲に正比例します。 SQLの場合、SQLが提供するもののほとんどは、通常のプログラミング言語で見られるものとはまったく異なるため、議論は弱くなります。 スペクトルのもう一方の端には、事実上すべての機能がDSLをBASICに収束させているテンプレートシステムがあります。
ここで、一歩下がって、分離の概念によって要約された、これら3つの典型的な摩擦の原因について考えてみましょう。 DSLは別個のものであるため、DSLは別個のファイルに配置する必要があります。 変更するのは難しく(そして自分で書くのはさらに難しい)、(常にではありませんが)実際のプログラミング言語に欠けている機能を1つずつ追加する必要があります。
分離性は、どんなにうまく設計されていても、DSLに固有の問題です。
次に、宣言型ツールの2番目の問題に目を向けます。これは、広く普及していますが、固有のものではありません。
別の問題:展開の欠如は複雑さにつながる
数か月前にこの記事を書いていたら、このセクションは「ほとんどの宣言型ツールは#@!$#@!」と名付けられていたでしょう。 複雑ですが、理由はわかりません。 この記事を書く過程で、私はそれを置くためのより良い方法を見つけました:ほとんどの宣言型ツールは必要以上に複雑です。 このセクションの残りの部分では、その理由を説明します。 ツールの複雑さを分析するために、複雑さのギャップと呼ばれる尺度を提案します。 複雑さのギャップは、ツールを使用して特定の問題を解決することと、ツールが置き換えることを意図している下位レベル(おそらく、単純な命令型コード)で問題を解決することの違いです。 前者のソリューションが後者よりも複雑な場合、複雑さのギャップが存在します。 より複雑なことは、コードの行数が増えることを意味します。コードは読みにくく、変更しにくく、保守しにくいものですが、必ずしもこれらすべてを同時に行う必要はありません。
低レベルのソリューションを可能な限り最高のツールと比較しているのではなく、ツールがない場合と比較していることに注意してください。 これは、 「まず、害を及ぼさない」という医学的原則を反映しています。
複雑さのギャップが大きいツールの兆候は次のとおりです。
- ツールの使用方法を知っている場合でも、命令型の用語で詳細に説明するのに数分かかるものは、ツールを使用してコーディングするのに数時間かかります。
- ツールではなく、常にツールを回避していると感じます。
- あなたはあなたが使用しているツールのドメインに正直に属する単純な問題を解決するのに苦労していますが、あなたが見つけた最良のスタックオーバーフローの答えは回避策を説明しています。
- この非常に単純な問題が特定の機能(ツールには存在しない)によって解決でき、ライブラリにGithubの問題があり、 +1秒が散在しているこの機能の長い議論が特徴である場合。
- 慢性的でかゆみがあり、ツールを捨てて、_for-loop_内ですべてを自分で行うことを切望しています。
テンプレートシステムはそれほど複雑ではないので、ここで感情の餌食になったかもしれませんが、この比較的小さな複雑さのギャップは設計のメリットではなく、適用範囲が非常に単純であるためです(ここではHTMLを生成しているだけです) )。 同じアプローチがより複雑なドメイン(構成管理など)に使用される場合は常に、複雑さのギャップによってプロジェクトがすぐに泥沼に変わる可能性があります。
とは言うものの、ツールが置き換えようとしている下位レベルよりもいくらか複雑であることは必ずしも受け入れられないわけではありません。 ツールがより読みやすく、簡潔で正確なコードを生成する場合は、それだけの価値があります。 ツールが置き換えられる問題よりも数倍複雑な場合、これは問題です。 これは完全に受け入れられません。 ブライアン・カーニハンは、次のように有名に述べています。「複雑さを制御することは、コンピュータープログラミングの本質です。 」ツールがプロジェクトにかなりの複雑さを加える場合、なぜそれを使用するのでしょうか。
問題は、なぜいくつかの宣言型ツールが必要以上に複雑なのかということです。 貧弱なデザインのせいにするのは間違いだと思います。 このような一般的な説明、これらのツールの作成者に対する全面的な人身攻撃は公平ではありません。 より正確で啓発的な説明が必要です。
私の主張は、下位レベルを抽象化するための高レベルのインターフェースを提供するツールは、下位レベルからこの上位レベルを展開する必要があるということです。 展開の概念は、クリストファー・アレクサンダーの最高傑作である「秩序の性質」、特に第2巻に由来しています。 ソフトウェア設計に対するこの記念碑的な作業の影響を要約することは、(私の理解は言うまでもなく)この記事の範囲を超えています(願わくば)。 その影響は今後数年で大きくなると思います。 展開プロセスの厳密な定義を提供することも、この記事を超えています。 ここでは、この概念をヒューリスティックな方法で使用します。
展開プロセスとは、段階的に、既存の構造を否定することなく、さらなる構造を作成するプロセスです。 以前の構造が単純に過去の変更の結晶化されたシーケンスである場合、すべてのステップで、各変更(またはアレクサンダーの用語を使用するための差別化)は以前の構造と調和したままです。
興味深いことに、Unixは、下位レベルから上位レベルへの展開の優れた例です。 Unixでは、オペレーティングシステムの2つの複雑な機能であるバッチジョブとコルーチン(パイプ)は、基本的なコマンドの単なる拡張です。 すべてをバイトストリームにするなど、特定の基本的な設計上の決定により、シェルはユーザーランドプログラムおよび標準I / Oファイルであるため、Unixはこれらの高度な機能を最小限の複雑さで提供できます。
これらが展開の優れた例である理由を強調するために、Unixの著者の1人であるDennisRitchieによる1979年の論文の抜粋をいくつか引用したいと思います。
バッチジョブの場合:
…新しいプロセス制御スキームにより、いくつかの非常に価値のある機能を実装するのが簡単になりました。 たとえば、分離されたプロセス(
&
を使用)やコマンドとしてのシェルの再帰的な使用。 ほとんどのシステムは、インタラクティブに使用されるファイルとは異なるファイルに対して、ある種の特別なbatch job submission
機能と特別なコマンドインタープリターを提供する必要があります。
コルーチンについて:
Unixパイプラインの天才は、シンプレックス方式で常に使用されるのとまったく同じコマンドから構築されていることです。
この優雅さとシンプルさは、展開するプロセスから来ていると私は主張します。 バッチジョブとコルーチンは、以前の構造から展開されます(コマンドはユーザーランドシェルで実行されます)。 Unixを作成したチームのミニマリストの哲学と限られたリソースのおかげで、システムは段階的に進化し、そのため、十分なリソースがなかったため、基本的な機能に戻ることなく高度な機能を組み込むことができたと思います。それ以外の場合は行います。
展開プロセスがない場合、高レベルは必要以上に複雑になります。 言い換えれば、ほとんどの宣言型ツールの複雑さは、それらの高レベルが、それらが置き換えることを意図している低レベルから展開されないという事実に起因します。
造語を許せば、この展開の欠如は、ユーザーを下位レベルから保護する必要性によって日常的に正当化されます。 ポカヨケ(低レベルのエラーからユーザーを保護する)に重点を置くと、複雑さが増すと新しいクラスのエラーが発生するため、複雑さのギャップが大きくなり、自己敗北します。 怪我に侮辱を加えるために、これらのクラスのエラーは問題のドメインとは関係がなく、ツール自体と関係があります。 これらのエラーを医原性と表現すれば、行き過ぎではありません。
宣言型テンプレートツールは、少なくともHTMLビューを生成するタスクに適用される場合、置き換えようとしている低レベルに戻る高レベルの典型的なケースです。 どうして? 自明でないビューを生成するにはロジックが必要であり、テンプレートシステム、特にロジックのないシステムでは、メインドアからロジックを削除し、その一部を猫のドアから密輸します。
注:複雑さのギャップが大きい場合の正当性はさらに弱くなります。ツールが魔法として販売されている場合、または機能するものの場合、魔法のツールは常に理解していなくても機能するはずなので、低レベルの不透明度は資産となるはずです。なぜまたはどのように。 私の経験では、ツールが魔法のようであるほど、それが私の熱意を欲求不満に変えるのが速くなります。
しかし、関心の分離についてはどうでしょうか。 ビューとロジックを分離したままにすべきではありませんか? ここでの主な間違いは、ビジネスロジックとプレゼンテーションロジックを同じバッグに入れることです。 ビジネスロジックは確かにテンプレートには存在しませんが、それでもプレゼンテーションロジックは存在します。 テンプレートからロジックを除外すると、プレゼンテーションロジックがサーバーにプッシュされ、サーバーが不自然に収容されます。 この点の明確な定式化は、この記事で優れた事例を示しているAlexeiBoronineのおかげです。
テンプレートの作業の約3分の2はプレゼンテーションロジックにあり、残りの3分の1は文字列の連結、タグの終了、特殊文字のエスケープなどの一般的な問題を扱っていると思います。 これは、HTMLビューを生成するための両面の低レベルの性質です。 テンプレートシステムは後半を適切に処理しますが、前半はうまくいきません。 ロジックのないテンプレートは、この問題に背を向け、厄介な解決を余儀なくされます。 他のテンプレートシステムは、ユーザーが実際にプレゼンテーションロジックを記述できるように、重要なプログラミング言語を提供する必要があるために問題があります。
総括する; 宣言型テンプレートツールは、次の理由で問題が発生します。
- 問題の領域から展開する場合は、論理パターンを生成する方法を提供する必要があります。
- ロジックを提供するDSLは、実際にはDSLではなく、プログラミング言語です。 構成管理などの他のドメインも、「展開」の欠如に悩まされていることに注意してください。
この記事のスレッドから論理的に切り離されているが、その感情的な核心に深く共鳴している議論で批評を締めくくりたいと思います。私たちは学ぶ時間が限られています。 人生は短く、それに加えて、私たちは働く必要があります。 私たちの限界に直面して、私たちは、急速に変化するテクノロジーに直面しても、有用で時間に耐えられるものを学ぶことに時間を費やす必要があります。 そのため、解決策を提供するだけでなく、実際にそれ自体の適用可能性の領域に明るい光を当てるツールを使用することをお勧めします。 RDBはデータについて教え、UnixはOSの概念について教えてくれますが、展開されない不十分なツールを使用すると、問題の性質について暗闇にとどまりながら、次善のソリューションの複雑さを学んでいると常に感じていました。それは解決するつもりです。
私が検討することをお勧めするヒューリスティックは、意図された機能の背後にある問題ドメインを覆い隠すツールではなく、問題ドメインを明らかにする価値ツールです。
ツインアプローチ
ここで紹介した宣言型プログラミングの2つの問題を克服するために、次の2つのアプローチを提案します。
- データ構造ドメイン固有言語(dsDSL)を使用して、分離を克服します。
- 複雑さのギャップを克服するために、低レベルから展開する高レベルを作成します。
dsDSL
データ構造DSL(dsDSL)は、プログラミング言語のデータ構造で構築されたDSLです。 中心的なアイデアは、文字列、数値、配列、オブジェクト、関数など、利用可能な基本的なデータ構造を使用し、それらを組み合わせて特定のドメインを処理するための抽象化を作成することです。
これらの構造(低レベル)を実装するパターンを指定することなく、構造またはアクション(高レベル)を宣言する力を維持したいと考えています。 DSLとプログラミング言語の分離を克服して、必要なときにいつでもプログラミング言語の全機能を自由に使用できるようにしたいと考えています。 これは可能であるだけでなく、dsDSLを介して簡単に実行できます。
1年前に聞いたら、dsDSLの概念は斬新だと思っていたでしょうが、ある日、JSON自体がこのアプローチの完璧な例であることに気づきました。 解析されたJSONオブジェクトは、DSLの利点を活用すると同時に、プログラミング言語内からの解析と処理を容易にするために、データエントリを宣言的に表すデータ構造で構成されます。 (他にもdsDSLがあるかもしれませんが、今のところ出会ったことはありません。ご存知の方は、コメント欄に記載していただければ幸いです。)
JSONと同様に、dsDSLには次の属性があります。
- これは非常に小さな関数のセットで構成されています。JSONには、
parse
とstringify
という2つの主要な関数があります。 - その関数は、最も一般的に複雑で再帰的な引数を受け取ります。解析されたJSONは配列またはオブジェクトであり、通常、内部にさらに配列とオブジェクトが含まれています。
- これらの関数への入力は、非常に特殊な形式に準拠しています。JSONには、有効な構造と無効な構造を区別するための明示的で厳密に適用された検証スキーマがあります。
- これらの関数の入力と出力の両方を、個別の構文なしでプログラミング言語に含めて生成することができます。
しかし、dsDSLは多くの点でJSONを超えています。 Javascriptを使用してHTMLを生成するためのdsDSLを作成しましょう。 後で、このアプローチを他の言語に拡張できるかどうかの問題に触れます(ネタバレ:RubyとPythonで確実に実行できますが、Cでは実行できない可能性があります)。
HTMLは、山かっこ( <
および>
)で区切られたtags
で構成されるマークアップ言語です。 これらのタグには、オプションの属性とコンテンツが含まれる場合があります。 属性は単にキー/値属性のリストであり、コンテンツはテキストまたは他のタグのいずれかです。 属性とコンテンツはどちらも、特定のタグのオプションです。 少し簡略化していますが、正確です。
dsDSLでHTMLタグを表す簡単な方法は、次の3つの要素を持つ配列を使用することです。-タグ:文字列。 -属性:オブジェクト(プレーン、キー/値タイプ)またはundefined
(属性が不要な場合)。 -内容:文字列(テキスト)、配列(別のタグ)、またはundefined
(内容がない場合)。
たとえば、 <a href="views">Index</a>
は['a', {href: 'views'}, 'Index']
と書くことができます。
このアンカー要素をクラスlinks
を持つdiv
に埋め込みたい場合は、次のように記述できます。 ['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']]
。
同じレベルで複数のhtmlタグを一覧表示するには、それらを配列でラップします。
[ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]
同じ原則を、タグ内に複数のタグを作成する場合にも適用できます。
['body', [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]]
もちろん、このdsDSLからHTMLを生成しなければ、このdsDSLは私たちを遠ざけることはありません。 dsDSLを取得し、HTMLで文字列をgenerate
関数が必要です。 したがって、 generate (['a', {href: 'views'}, 'Index'])
を実行すると、文字列<a href="views">Index</a>
が取得されます。
DSLの背後にある考え方は、特定の構造を持ついくつかの構造を指定し、それを関数に渡すことです。 この場合、dsDSLを構成する構造はこの配列であり、1つから3つの要素があります。 これらの配列には特定の構造があります。 generate
が入力を徹底的に検証する場合(これらの検証ルールはDSLの構文の正確な類似物であるため、入力を徹底的に検証することは簡単で重要です)、入力のどこで問題が発生したかを正確に示します。 しばらくすると、dsDSLの有効な構造を区別するものがわかり始めます。この構造は、それが生成する基本的なものを強く示唆します。
さて、DSLとは対照的にdsDSLのメリットは何ですか?
- dsDSLは、コードの不可欠な部分です。 これにより、行数、ファイル数が減少し、オーバーヘッドが全体的に削減されます。
- dsDSLは解析が簡単です(したがって、実装と変更が簡単です)。 解析とは、配列またはオブジェクトの要素を反復処理することです。 同様に、dsDSLは、新しい構文(誰もが嫌う)を作成する代わりに、プログラミング言語の構文(誰もが嫌いですが、少なくともすでに知っている)に固執できるため、比較的簡単に設計できます。
- dsDSLには、プログラミング言語のすべての機能があります。 これは、dsDSLを適切に使用すると、高レベルツールと低レベルツールの両方の利点があることを意味します。
さて、最後の主張は強いものなので、このセクションの残りの部分をそれをサポートするために費やします。 適切に雇用されているとはどういう意味ですか? これが実際に動作することを確認するために、 DATA
という名前の配列からの情報を表示するテーブルを作成する例を考えてみましょう。

var DATA = [ {id: 1, description: 'Product 1', price: 20, onSale: true, categories: ['a']}, {id: 2, description: 'Product 2', price: 60, onSale: false, categories: ['b']}, {id: 3, description: 'Product 3', price: 120, onSale: false, categories: ['a', 'c']}, {id: 4, description: 'Product 4', price: 45, onSale: true, categories: ['a', 'b']} ]
実際のアプリケーションでは、 DATA
はデータベースクエリから動的に生成されます。
さらに、 FILTER
変数があります。これは、初期化されると、表示するカテゴリの配列になります。
テーブルに次のことを行います。
- テーブルヘッダーを表示します。
- 製品ごとに、説明、価格、カテゴリのフィールドを表示します。
-
id
フィールドは出力せず、各行のid
属性として追加します。 代替バージョン:各tr
要素にid
属性を追加します。 - 製品が販売されている場合は、クラス
onSale
に配置します。 - 価格の降順で商品を並べ替えます。
- カテゴリで特定の製品をフィルタリングします。
FILTER
が空の配列の場合、すべての製品が表示されます。 それ以外の場合は、商品のカテゴリがFILTER
に含まれている商品のみを表示します。
この要件に一致するプレゼンテーションロジックを約20行のコードで作成できます。
function drawTable (DATA, FILTER) { var printableFields = ['description', 'price', 'categories']; DATA.sort (function (a, b) {return a.price - b.price}); return ['table', [ ['tr', dale.do (printableFields, function (field) { return ['th', field]; })], dale.do (DATA, function (product) { var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; }); return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]]; })]; }) ]]; }
これは単純な例ではないことを認めますが、永続ストレージの4つの基本機能(CRUDとも呼ばれます)のかなり単純なビューを表しています。 重要なWebアプリケーションには、これよりも複雑なビューがあります。
このコードが何をしているのか見てみましょう。 まず、製品テーブルを描画するためのプレゼンテーションロジックを含む関数drawTable
を定義します。 この関数は、パラメーターとしてDATA
とFILTER
を受け取るため、さまざまなデータセットとフィルターに使用できます。 drawTable
は、partialとhelperの2つの役割を果たします。
var drawTable = function (DATA, FILTER) {
内部変数printableFields
は、どのフィールドが印刷可能であるかを指定する必要がある唯一の場所であり、要件の変化に直面した場合の繰り返しや不整合を回避します。
var printableFields = ['description', 'price', 'categories'];
次に、製品の価格に従ってDATA
を並べ替えます。 プログラミング言語全体を自由に使用できるため、さまざまでより複雑な並べ替え基準を簡単に実装できることに注意してください。
DATA.sort (function (a, b) {return a.price - b.price});
ここでは、オブジェクトリテラルを返します。 最初の要素としてtable
を含み、2番目の要素としてその内容を含む配列。 これは、作成する<table>
のdsDSL表現です。
return ['table', [
ここで、テーブルヘッダーを使用して行を作成します。 そのコンテンツを作成するには、Array.mapのような関数であるdale.doを使用しますが、オブジェクトに対しても機能します。 printableFields
を繰り返し、それぞれのテーブルヘッダーを生成します。
['tr', dale.do (printableFields, function (field) { return ['th', field]; })],
HTML生成の主力であるイテレーションを実装したばかりであり、DSL構造は必要ありませんでした。 データ構造を反復してdsDSLを返す関数だけが必要でした。 同様のネイティブ関数、またはユーザーが実装した関数でも、このトリックを実行できます。
次に、 DATA
に含まれる製品を繰り返し処理します。
dale.do (DATA, function (product) {
この商品がFILTER
によって除外されているかどうかを確認します。 FILTER
が空の場合、製品を印刷します。 FILTER
が空でない場合は、 FILTER
に含まれているものが見つかるまで、製品のカテゴリーを繰り返し処理します。 これは、dale.stopを使用して行います。
var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; });
条件の複雑さに注意してください。 それは私たちの要件に正確に合わせられており、DSLではなくプログラミング言語を使用しているため、それを表現するための完全な自由があります。
matches
がfalse
の場合、空の配列を返します(したがって、この製品は出力しません)。 それ以外の場合は、適切なIDとクラスを使用して<tr>
を返し、 printableFields
を反復処理してフィールドを出力します。
return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]];
もちろん、開いたものはすべて閉じます。 構文は面白くないですか?
})]; }) ]]; }
では、このテーブルをより広いコンテキストにどのように組み込むのでしょうか。 ビューを生成するすべての関数を呼び出すdrawAll
という名前の関数を記述します。 drawTableとは別に、 drawTable
、 drawHeader
、およびその他の同等の関数があり、これらはすべてdrawFooter
を返します。
var drawAll = function () { return generate ([ drawHeader (), drawTable (DATA, FILTER), drawFooter () ]); }
上記のコードがどのように見えるかが気に入らない場合は、私が言うことは何もあなたを納得させません。 これは最高のdsDSLです。 あなたは記事を読むのをやめたほうがいいかもしれません(そしてあなたがこれまでにそれを成し遂げたならあなたがそうする権利を獲得したので平均的なコメントも落としてください!)。 But seriously, if the code above doesn't strike you as elegant, nothing else in this article will.
For those who are still with me, I would like to go back to the main claim of this section, which is that a dsDSL has the advantages of both the high and the low level :
- The advantage of the low level resides in writing code whenever we want, getting out of the straightjacket of the DSL.
- The advantage of the high level resides in using literals that represent what we want to declare and letting the functions of the tool convert that into the desired end state (in this case, a string with HTML).
But how is this truly different from purely imperative code? I think ultimately the elegance of the dsDSL approach boils down to the fact that code written in this way mostly consists of expressions, instead of statements. More precisely, code that uses a dsDSL is almost entirely composed of:
- Literals that map to lower level structures.
- Function invocations or lambdas within those literal structures that return structures of the same kind.
Code that consists mostly of expressions and which encapsulate most statements within functions is extremely succinct because all patterns of repetition can be easily abstracted. You can write arbitrary code as long as that code returns a literal that conforms to a very specific, non-arbitrary form.
A further characteristic of dsDSLs (which we don't have time to explore here) is the possibility of using types to increase the richness and succinctness of the literal structures. I will expound on this issue on a future article.
Might it be possible to create dsDSLs beyond Javascript, the One True Language? I think that it is, indeed, possible, as long as the language supports:
- Literals for: arrays, objects (associative arrays), function invocations, and lambdas.
- Runtime type detection
- Polymorphism and dynamic return types
I think this means that dsDSLs are tenable in any modern dynamic language (ie: Ruby, Python, Perl, PHP), but probably not in C or Java.
Walk, Then Slide: How To Unfold The High From The Low
In this section I will attempt to show a way for unfolding a high level tool from its domain. In a nutshell, the approach consists of the following steps
- Take two to four problems that are representative instances of a problem domain. These problems should be real. Unfolding the high level from the low one is a problem of induction, so you need real data to come up with representative solutions.
- Solve the problems with no tool in the most straightforward way possible.
- Stand back, take a good look at your solutions, and notice the common patterns among them.
- Find the patterns of representation (high level).
- Find the patterns of generation (low level).
- Solve the same problems with your high level layer and verify that the solutions are indeed correct.
- If you feel that you can easily represent all the problems with your patterns of representation, and the generation patterns for each of these instances produce correct implementations, you're done. Otherwise, go back to the drawing board.
- If new problems appear, solve them with the tool and modify it accordingly.
- The tool should converge asymptotically to a finished state, no matter how many problems it solves. In other words, the complexity of the tool should remain constant, rather than growing with the amount of problems it solves.
Now, what the hell are patterns of representation and patterns of generation ? よろしくお願いします。 The patterns of representation are the patterns in which you should be able to express a problem that belongs to the domain that concerns your tool. It is an alphabet of structures that allows you to write any pattern you might wish to express within its domain of applicability. In a DSL, these would be the production rules. Let's go back to our dsDSL for generating HTML.
The patterns of representation for HTML are the following:
- A single tag:
['TAG']
- A single tag with attributes:
['TAG', {attribute1: value1, attribute2: value2, ...}]
- A single tag with contents:
['TAG', 'CONTENTS']
- A single tag with both attributes and contents:
['TAG', {attribute1: value1, ...}, 'CONTENTS']
- A single tag with another tag inside:
['TAG1', ['TAG2', ...]]
- A group of tags (standalone or inside another tag):
[['TAG1', ...], ['TAG2', ...]]
- Depending on a condition, place a tag or no tag:
condition ? ['TAG', ...] : []
/ Depending on a condition, place an attribute or no attribute:['TAG', {class: condition ? 'someClass': undefined}, ...]
These instances can be represented with the dsDSL notation we determined in the previous section. And this is all you need to represent any HTML you might need. More sophisticated patterns, such as conditional iteration through an object to generate a table, may be implemented with functions that return the patterns of representation above, and these patterns map directly to HTML tags.
If the patterns of representation are the structures you use to express what you want, the patterns of generation are the structures your tool will use to convert patterns of representation into the lower level structures. For HTML, these are the following:
- Validate the input (this is actually is an universal pattern of generation).
- Open and close tags (but not the void tags, like
<input>
, which are self-closing). - Place attributes and contents, escaping special characters (but not the contents of the
<style>
and<script>
tags).
Believe it or not, these are the patterns you need to create an unfolding dsDSL layer that generates HTML. Similar patterns can be found for generating CSS. In fact, lith does both, in ~250 lines of code.
One last question remains to be answered: What do I mean by walk, then slide ? When we deal with a problem domain, we want to use a tool that delivers us from the nasty details of that domain. In other words, we want to sweep the low level under the rug, the faster the better. The walk, then slide approach proposes exactly the opposite: spend some time on the low level. Embrace its quirks, and understand which are essential and which can be avoided in the face of a set of real, varied, and useful problems.
After walking in the low level for some time and solving useful problems, you will have a sufficiently deep understanding of their domain. The patterns of representation and generation will then arise naturally; they are wholly derived from the nature of the problem they intend to solve. You can then write code that employs them. If they work, you will be able to slide through problems where you recently had to walk through them. Sliding means many things; it implies speed, precision and lack of friction. Maybe more importantly, this quality can be felt; when solving problems with this tool, do you feel like you're walking through the problem, or do you feel that you're sliding through it?
Maybe the most important thing about an unfolded tool is not the fact that it frees us from having to deal with the low level. Rather, by capturing the empiric patterns of repetition in the low level, a good high level tool allows us to understand fully the domain of applicability.
An unfolded tool will not just solve a problem - it will enlighten you about the problem's structure.
So, don't run away from a worthy problem. First walk around it, then slide through it.