デモのコンテスト(視聴覚シリーズを生成するプログラム。その主な機能は非常に小さいサイズです-数十またはキビバイト単位)。
一般的な議論の過程で、誰かがスクリプト言語でプログラムを書くという、非標準的なデモのアイデアを世界に提案しました。 実際、すべてのデモは、サイズを縮小するためにパッカーによって圧縮されています(実行中に解凍されます)。 また、テキストはバイナリコードよりもはるかに圧縮されています。 インタプリタが非常に小さい場合、これは大きな利点をもたらします。
フロントエンドでの経験から、すぐにコードをさらに小さくするというアイデアを得ました。スペースとオプション要素を削除し、識別子の長さを減らします。 実際、圧縮によりすべての情報が保存され、構文の多くの要素は必要ありません。
しかし、それでも、既存の言語のほとんどはこの最適化のために設計されていません。明らかに、機械ではなく人が理解するために必要な多くの要素があります。 しかし、縮小化専用に設計された言語を開発したらどうでしょうか?
その結果、私はその競争には参加しませんでした。 しかし、この考えは私を離れませんでした。 結局のところ、それはデモよりも実用的な目的に役立つ可能性があります-フロントエンドの世界では、クライアントスクリプトの量は依然として非常に重要であり、それを減らすことができれば、このソリューションは少なくともいくつかの場合に正当化されるかもしれません。
言語のプロトタイプを作成し、その結果を確認するために、実験を行うことにしました。
主な機能
次の主要な機能を言語に追加しました:動的型付け、機能的パラダイム、区切り文字の欠如、および識別子クラス。
静的な型付けでは型を記述する必要があり、貴重なスペースを占有するため、動的な型付けを選択しました。
機能的パラダイム-表現力が高いため(より少ないコードでより多くのアルゴリズムを表現できるため)。
他の2つについて詳しく説明します。
セパレーターの欠如
次のコードを検討してください。
add(2, multiple(2, 2))
各関数が期待する引数の数(関数のアリティ)は事前にわかっています。 ここの括弧とコンマは、読みやすくするためだけのものです。 それらを削除します。
add 2 multiple 2 2
演算子でも同じことを行うことができますが、それらの機能を考慮するだけです:
+ 2 * 2 2
同時に、オペレーターは優先順位を持っている必要はありません-関数を呼び出すには、まずすべてのパラメーターを計算する必要があるため、アクションの順序は記録順序によって決まります。 したがって、上記の式は値6を返します。8を取得するには、次のようにコードを記述する必要があります。
* 2 + 2 2
識別子クラス
私の演算子は通常の関数なので、これを拡張し、識別子に句読点文字を使用できるようにすることにしました。
そして、すべての識別子をアルファベットと句読点の2つのクラスに分割するというアイデアを得ました。
実際、どの言語でも、識別子は何かで区切る必要があります-他の構文要素または空白文字によって。 それ以外の場合はあいまいになります: xy
は識別子xy
または2つの識別子x
とy
ですか?
ただし、2つのばらばらの識別子クラスがあるため、この要件を緩和できます。異なるクラスの識別子を一緒に記述でき、あいまいさはありません。
さらに、私の最初のクラスがアルファベットのみであるということは何もありません-数字は含まれていません。 これにより、任意のクラスの識別子と一緒に数字を書くことができます。
たとえば、次の式を使用します。
foo($(5))
私の言語では、次のように書くことができます。
foo$5
問題解決
言語を開発する過程で、いくつかの興味深い問題が発生しましたが、私は予期していませんでしたが、まだ解決できました。
引数なしで関数を呼び出す
他の言語のように、関数を呼び出すための特別な構文はないので、引数のない関数は、名前を示すだけで呼び出す必要があります。
fn answer() 42 ; answer
それでうまくいきましたが、ネストのレベルは1つだけでした。 そのような関数が別の関数を返すとどうなりますか
fn answer() fn() 42 ; ; answer
その場合、結果は内部的な意味ではなく、閉鎖になります。 そして、この回路を引き起こす方法は? 結局のところ、私たちはすでに彼の名前を示しました。
Clojure言語のアプローチを使用する必要がありました-トランポリン:計算後の値は、結果が別のものになるまで、引数を必要としない閉包を周期的に引き起こす特別なサイクルに入ります。 したがって、上記の2番目の例の結果も、最初の例のように42になります。
コード構造の構築
ミニフィケーションを実装できるようにするには、実行せずにコードの構造を決定できる必要があります。
そして、すべての関数の引数の数がわかれば、簡単です。 たとえば、次のコード:
add 2 multiple 2 2
次の構造になっています。
ただし、クロージャを返し始めるとすぐに、あいまいさが現れます。
fn add(xy) + xy ; fn increase(x) + x 1 ; fn foo(n) if == n 2 add increase ; foo x ...
foo
関数を呼び出した結果に渡す引数はいくつですか? これは、コードの実行中にのみ決定できますが、分析の段階では決定できません。 そして、これは縮小を不可能にします。
この問題を解決するために、タイピングを準静的に拡張しました:型は関数に対してのみ指定する必要がありますが、型は関数自体とその結果(クロージャーの場合)に必要な引数の数によってのみ示されます。
fn make_adder(bias):2 fn(xy) + bias + xy ; ; make_adder 1 2 2
make_adder
関数のmake_adder
は、結果がクロージャであり、2つのパラメーターが必要であることを明示的に示しています。 したがって、最後の呼び出しの構造を簡単に構築できます。
共通の機能
この言語には、 nil
、浮動小数点数、リスト、ハッシュテーブル、およびクロージャーのタイプがあります。 行はリストに基づいて実装されます。 論理値はありません-残りのタイプの特定の値はfalseとみなされ、他のすべての値はtrueとみなされます。
この言語には一連の組み込み関数があります。基本的な数学関数と演算(ビット演算を含む)、リストとハッシュテーブルを操作する関数、基本的な入出力です。
この言語は、ソースコードファイルの動的な読み込みを通じてモジュール性をサポートします。 サポートされているキャッシュ、 vendor
ディレクトリ、および特別な環境変数で指定されたパスでの検索。
この言語には、機能スタイルのリストを操作するためのモジュールと単体テスト用のモジュールを含む小さな標準ライブラリもあります。
ベンチマーク
縮小の可能性を評価するために、私は自分の言語をJavaScriptと比較することにしました。 これを行うために、私は両方に同じプログラムを書きました。
タスクとして、仮想DOMツリー比較アルゴリズムを選択しました。 私はこれらの記事を基礎としました:
ただし、それらでは、比較すると、実際のDOMがすぐに変更されるため、必要な変更のリストを、それらが属するノードのアドレスとともに生成しました。
function make_node(type, properties, ...children) { return { 'type': type, 'properties': properties || {}, 'children': children, } } function make_difference(path, action, ...parameters) { return { 'path': path, 'action': action, 'parameters': parameters, } } function is_different(node_1, node_2) { return typeof node_1 !== typeof node_2 || (typeof node_1 === 'object' ? node_1['type'] !== node_2['type'] : node_1 !== node_2) } function compare_property(path, name, old_value, new_value) { let difference if (!new_value) { difference = make_difference(path, 'remove_property', name) } else if (!old_value || new_value !== old_value) { difference = make_difference(path, 'set_property', name, new_value) } return difference } function compare_properties(path, old_properties, new_properties) { const properties = Object.assign({}, old_properties, new_properties) return Object.keys(properties) .map(name => compare_property(path, name, old_properties[name], new_properties[name])) .filter(difference => difference) } function compare_nodes(old_node, new_node, index=0, path=[]) { let differences = [] if (!old_node) { differences.push(make_difference(path, 'create', new_node)) } else if (!new_node) { differences.push(make_difference(path, 'remove', index)) } else if (is_different(old_node, new_node)) { differences.push(make_difference(path, 'replace', index, new_node)) } else if (old_node['type']) { const child_path = [...path, old_node['type']] const properties_differences = compare_properties(child_path, old_node['properties'], new_node['properties']) differences.push(...properties_differences) const maximal_children_length = Math.max(old_node['children'].length, new_node['children'].length) for (let i = 0; i < maximal_children_length; i++) { const child_differences = compare_nodes(old_node['children'][i], new_node['children'][i], i, child_path) differences.push(...child_differences) } } return differences } module['exports'] = { 'make_node': make_node, 'compare_nodes': compare_nodes, }
let map:2 load "std/list/map"; let filter:2 load "std/list/filter"; let zip_longest:3 load "std/list/zip_longest"; let reduce:3 load "std/list/reduce"; fn make_node(kind properties children) #"type" kind #"properties" properties #"children" children {}; fn make_difference(path action parameters) #"path" path #"action" action #"parameters" parameters {}; fn is_different(node_i node_ii) || != type node_i type node_ii fn() if == "hash" type node_i fn() != ."type"node_i ."type"node_ii; fn() != node_i node_ii; ; ; fn compare_property(path name old_value new_value) if !new_value fn() make_difference path "remove_property" ,name[]; fn() if || !old_value != new_value old_value fn() make_difference path "set_property" ,name,new_value[]; fn() nil; ; ; fn compare_properties(path old_properties new_properties) let properties + old_properties new_properties; let differences map keys properties fn(name) compare_property path name .name old_properties .name new_properties ;; filter differences fn(difference) difference ; ; fn compare_nodes(old_node new_node index path) if !old_node fn() , make_difference path "create" ,new_node[] []; fn() if !new_node fn() , make_difference path "remove" ,index[] []; fn() if is_different old_node new_node fn() , make_difference path "replace" ,index,new_node[] []; fn() if == "hash" type old_node fn() let child_path + path , ."type"old_node []; let properties_differences compare_properties child_path ."properties"old_node ."properties"new_node ; let children_pairs zip_longest ."children"old_node ."children"new_node fn(node_i node_ii) ,node_i,node_ii[] ; ; let children_differences let result reduce {} children_pairs fn(result children_pair) let index ?? ."index"result 0; let differences compare_nodes .0 children_pair .1 children_pair index child_path ; #"differences" + ?? ."differences"result [] differences #"index" ++ index {}; ; ?? ."differences"result [] ; + properties_differences children_differences ; fn() []; ; ; ; ; #"make_node" fn(kind properties children) make_node kind properties children ; #"compare_nodes" fn(old_node new_node index path) compare_nodes old_node new_node index path ; {}
Google Closure Compiler ( JavaScriptバージョン )を使用してJavaScriptバージョンを縮小しました。私の言語のバージョンは手動です。
結果:
パラメータ | Javascript | 私の舌 |
---|---|---|
完全版ボリューム | 2398 B | 2827 B |
ボリューム縮小版 | 794 B | 872 B |
ボリューム節約 | 66.89% | 69.16% |
まとめ
私の考えが理にかなっているためには、圧縮でJavaScriptを数回倒す必要がありました(結局、インタープリター自体の場所が必要です)。 そして結果はさらに大きくなりました。
したがって、実験は失敗に終わりました。 私が言語の基礎を築いたアイデアは、期待した利益をもたらさなかった。
リポジトリ
インタープリターのソースコード(Pythonで実装)、標準ライブラリとサンプル、ドキュメントは、MITライセンスの下でリポジトリから入手できます。