Back to list
Sep 6 2017
CX

CXの概要

序文

CXとは、アフォーダンスの概念に基づいた新しいプログラミングパラダイムを採用して設計された、仕様とプログラミング言語の両方を指します。 アフォーダンスは、何ができて、何ができないのかをプログラムが知ることを可能にします。 たとえば、関数にどのような引数を送ることができるかをプログラムに照会することができ、プログラムは可能なアクションのリストを返します。 リストからどのようなアクションが適切であるかを決定した後、選択肢の1つを選び、そのアクションをプログラムが実行します。 CXのアフォーダンスシステムの重要な要素は、遺伝的プログラミングアルゴリズムが構築されると、実行時にプログラムの構造を最適化するために使用できるネイティブ関数として提供される事です。

CXの仕様では、プログラマはコンパイラとインタプリタの両方にアクセス可能でなければならないとされています。 インタプリタは、プログラマが対話的に要素をプログラムに追加したり削除したりできるREPL(Read-eval-print loop)を通じてアクセスできます。 プログラムが完成すると、そのパフォーマンスを向上させるためにコンパイルすることができます。

CXの型システムは非常に厳格です。 唯一の “暗黙の型変換"は、パーサーが整数、浮動小数点数、ブール値、文字列、または配列を判別するときに発生します。 たとえば、関数が64ビット整数を必要とする場合、明示的に必要な型に変換するためには、キャスト関数を使用する必要があります。

最後に、CXプログラムをバイト配列に完全にシリアライズして、実行状態と構造を維持することができます。 このシリアライズされたバージョンのプログラムは、後でデシリアライズして、CXインタープリタ/コンパイラを持つ任意のデバイスでその実行を再開することができます。

以下のセクションでは、上で説明したCXの機能について詳しく説明します。

プロジェクトのリポジトリ

プロジェクトのソースコードは、Githubリポジトリhttps://github.com/skycoin/cx からダウンロードできます。 リポジトリには、仕様ファイル、ドキュメント、サンプル、およびソースコードが含まれています。

構文

はじめに述べたように、CXは仕様とプログラミング言語の両方を指します。 CX仕様は構文を規定するのではなく、むしろCXとみなすためにCX言語が実装しなければならない構造とプロセスを規定しています。 結果として、2つのCX言語を実装することができ、1つはLispのような構文で、もう1つはCのような構文です。 この基礎となる言語は、CX Base、すなわち「基本言語」と呼ばれています。 このドキュメントでは、実装は仕様の機能を示すために使用されていますが、その目的は学術ツールとしての機能だけでなく、一般的な目的に使用できる完全で堅牢な言語となることです。

このドキュメントで使用されているCXは、Goの構文にできるだけ似た構文を持つことを目標としています。

アフォーダンス

プログラマは、関数が受け取るパラメータの数、戻すパラメータの数、目的の機能を得るために必要な記述、文関数にパラメータとして送る必要がある引数など、プログラムを構築する際に非常に多くの決定をする必要があります。 CXのアフォーダンスシステムでは、要素に適用できる可能なアクションのリストを取得するための照会を受けることができます。 この文脈では、要素の例は、関数、構造体、モジュール、および式です。

プログラムの背後にある、ロジックと目的が何であるべきかを指示する一連のルールとファクトを持たずに、少なくともプログラムが意味論的に正しいことを保証する基本的な制約をいくつか決めることができます。 アフォーダンスシステムは、第1のフィルタリング層として、以下で説明する制約を提供します。

引数の個数の制約

CXの式は複数の値を返すことができます。 これは、式の出力引数を受け取る変数の数が、式の演算子で定義された出力の数と一致する必要があるため、アフォーダンスシステムの仕事となります。

out1, out2, ..., outN := op(inp1, inp2, ..., inpM)

上記の例が正しい場合、opN個の引数を出力する必要があります。 opの定義がアフォーダンスシステム自体またはユーザーによって将来変更され得ることを考慮すると、この問題はさらに複雑になり得ます。 opの定義が変更されるとすぐに、opの出力引数を受け取る変数の数が不一致になるため、新しいアフォーダンスがopを演算子として使っているどんな式にも適用されます。

前のロジックはまた、受信変数の数が式の演算子の出力パラメータの数と一致する場合、新しい受信変数を追加する動作がもはや実行できないことを意味します。

引数の個数の制約は、式の入力引数にも適用されます。 つまり、関数呼び出しのすべての入力引数がすでに定義されている場合、アフォーダンスシステムは、別の引数を可能なアクションとして追加する必要がありません。 同様に、式が必要な数よりも少ない引数を持つ演算子を呼び出そうとしている場合、アフォーダンスシステムは照会を受けた時に、新しい引数を関数呼び出しに追加できることをプログラマに伝える必要があります。

例:

注:文字列の連結はまだ実装されていません。 また、print関数は、表示される文字列の最後に常に改行を追加します。 このドキュメントで紹介するCX実装の将来のバージョンでは、これらの問題が解決されます。

var age i32 = 18
var steps i32 = 23

func advance (direction str, numberSteps i32) () {
    printStr("Advancing:")
    printStr(direction)
    printStr("Number of steps:")
    printI32(numberSteps)
}

func main () () {
    advance("North")
}

上記の例では、main関数内のadvanceの呼び出しで引数が1つ不足しています。 アフォーダンスシステムに照会すると、システムは次のような行動を取ります。

...
(k)       AddArgument advance age
(k+1)     AddArgument advance steps
...

ここでkは任意のインデックスを表します。 アフォーダンスシステムは、2つのアクションが実行可能で、それらはadvance関数に別の引数を追加することをプログラマに告げており、グローバル変数のagestepsは引数となり得る選択肢です。

アフォーダンスは常に列挙されるべきであり、アフォーダンスシステムへの照会においてそれらの順序が一定であるべきことは重要です。 その理由は、プログラマが照会の結果を調べた後に、どのアフォーダンスを適用するのかをシステムに示す必要があるためです。

型の制約

プログラミング言語における共通の挙動は、プログラマーが予期しない型の引数を関数呼び出しに送ることを制限する型システムを持つことです。 たとえ弱く型付けされたプログラミング言語であっても、次の演算true / "hello world"はエラーを発生させるはずです(もちろん、難解プログラミング言語の場合を除いて). CXは非常に強い型付けシステムに従っており 、予想される型ではない引数は、アフォーダンスのアクションの候補と見なすべきではありません(回避策は、アフォーダンスとして表示される前にこれらの引数をキャスト関数で囲みます)。

すでに存在する変数に新しい値を割り当てる場合は、型の制約も考慮する必要があります。 CXでは、特定の型の宣言された変数は、全てのライフタイム中で(その型がメタプログラミングのコマンド/関数を使用して削除され、新たに作成されない限り)、その型のままでなければなりません。 したがって、32ビット整数を保持すると宣言された変数は、例えば64ビット浮動小数点出力引数を受け取る候補とはみなされません。

存在の制約

このタイプの制約は一見すると些細なことに思えるかもしれません。“要素が存在しない場合、その要素を含むアフォーダンスは存在してはならない” にもかかわらず、関数の名前が変更され、プログラム中の式の演算子としてすでに使用されている状況を考えれば、この制約は困難になります。 プログラムがソースコード形式であれば、この問題は単純な「検索と置換」プロセスになりますが、実行時には"演算子にバインドされた識別子を変更するアフォーダンス"として、アフォーダンスシステムが非常に便利になります。

要素の名前を変更しない場合でも、要素が存在するかどうかを判定することは簡単ではありません。 アフォーダンスで使用される要素は、コールスタックの現在のスコープ、グローバルスコープ、および他のモジュールのグローバルスコープで検索する必要があります。

識別子の制約

新しい名前付き要素を追加することは、一般的に、アフォーダンスの候補アクションです。 そのようなタイプのアフォーダンスを適用しようとするときに生じる制約は、再定義を避けるため新しい要素に一意の識別子を割り当てることです。 アフォーダンスシステムは、要素のスコープ内に固有の識別子を生成するか、またはプログラマに適切な識別子を提供するように要求することができます。

境界の制約

CXは、配列内の要素にアクセスし変更するためのネイティブ関数を提供します。 配列読み出しと配列書き込みの例は次のとおりです。

readI32([]i32{0, 10, 20, 30}, 3)
writeF32([]f32{0.0, 10.10, 20.20}, 1, 5.5)

最初の式では、4つの32ビット整数の配列がインデックス3でアクセスされ、配列の最後の要素が返されます。 2番目の式では、3つの32ビット浮動小数点数の配列の2番目の要素が5.5に変更されています。 これらの配列のいずれかが、負のインデックスまたは配列の長さを超えるインデックスを使用してアクセスされた場合、「境界外」エラーが発生します。

型の制約のみに従うことによって、アフォーダンスシステムは、任意の32ビット整数引数を任意の配列にアクセスするためのインデックスとして使用できることをプログラマに通知します。 これらのプログラムはコンパイルされますが、プログラマが特別な注意を払わなければ、境界外のエラーが発生する可能性は非常に高いです。

アフォーダンスシステムは、以下の基準に従ってアフォーダンスをフィルタリングする必要があります。 負の32ビット整数を破棄し、配列読み出し関数または書き込み関数に送信される配列の長さを超える32ビット整数を破棄します。

ユーザー定義の制約

注:ユーザー定義の制約システムはまだ実験段階です。

上記の基本的な制約は、プログラムがランタイムエラーに遭遇しないことを少なくとも保証するべきです。 これらの制約は、CXの固有の進化的アルゴリズムなどの興味深いシステムを構築するのに十分なものでなければなりません。 それにもかかわらず、場合によってはより堅牢なシステムが必要とされます。 この目的のために、節、クエリ、およびオブジェクトは、モジュールの環境を記述するために使用されます。 これらの要素は、統合されたPrologインタプリタと、CXネイティブ関数setClausessetQuery、およびaddObjectを使用して定義されます。

この制約システムの最も一般的な説明は、追加された各オブジェクトに対して、定義されたPrologクエリを使用して照会される一連のProlog句(ファクトとルール)をプログラマが定義することです。 この説明は、初めてこれを読んだ人にとっては、ほとんど意味不明でしょう。 次の例は、概念とプロセスをもう少し明確にしてくれるはずです。

setClauses("move(robot, north, X, R) :- X = northWall, R = false.")

setQuery("move(robot, %s, %s, R).")

この例では、1つのルールしか定義されていません。 このルールは、「ロボットが北に移動したい場合はXが何であるかを尋ねる。もしXがnorthWallだった場合は移動できない」と概ね解釈できます。 クエリはアクションmoveのクエリとして機能する書式文字列です。 それは方向とオブジェクトという2つの引数を受け取る要素robotのためのものです。

オブジェクトは、addObject関数を使用して定義できます。

addObject("southWall")
addObject("northWall")

制約システムは、モジュールに存在するオブジェクトのそれぞれについてシステムに照会します。 この例では、システムはまず「move(robot、north、southWall)」というクエリを実行し、システムは「nil」と応答します。 つまり、このような状況を処理するためのルールは定義されていません。 そしてデフォルトのアクションはアフォーダンスを破棄しません。 2番目のクエリは “move(robot、north、northWall)“になり、システムは “false"と応答します。 この場合、アフォーダンスはテストに合格しておらず、破棄されます。

上記の例は、これらのルールが条件を使用してアフォーダンスを否定する方法を示しています。 しかし、前のルールによって否定された後でも、ルールはアフォーダンスを受け入れるために使用できます。

setClauses("move(robot, north, X, R) :- X = northWall, R = false.
    move(robot, north, X, R) :- X = northWormhole, R = true.")

setQuery("move(robot, %s, %s, R).")

上記のコードで追加されたルールは、ワームホールが存在する場合、北に向かうロボットの動きを受け入れるようにシステムに指示します。 オブジェクト配列が前に定義した通りに残っていれば、移動アフォーダンスは破棄されますが、 addObject("northWormhole") が評価された場合は、「northWormhole」が追加され、ロボットはワームホールを使用して壁を通過することができます。

強い型付けシステム

はじめに述べたように、CXには暗黙のキャストはありません。 このため、各プリミティブ型の複数のバージョンがコアモジュールで定義されています。 たとえば、addI32、addI64、addF32、およびaddF64の4つのネイティブ関数が存在します。

パーサーは、ソースコード内で見つかったデータにデフォルトの型を付与します。 整数が読み取られた場合、デフォルトの型はi32または32ビット整数です。 浮動小数点が読み取られた場合、デフォルトの型はf32または32ビットfloatです。 パーサーが読み取る他のデータにはあいまいさはありません。 truefalse は常にブーリアンです。 ダブルクォーテーションで囲まれた一連の文字は常に文字列です。 配列は []i64{1, 2, 3}のように要素のリストの前にその型を示す必要があります。

プログラマがある型の値を別の型に明示的にキャストする必要がある場合、コアモジュールはプリミティブ型を扱うためにいくつかのキャスト関数を提供します。 たとえばbyteAToStrはバイト配列を文字列にキャストし、i32ToF32は32ビットの整数を32ビットのfloatにキャストします。

コンパイルと逐次実行

CX仕様では、開発者にインタプリタとコンパイラの両方を提供するためにCX言語が適用されています。 逐次実行されるプログラムは、コンパイルされたプログラムよりもはるかに遅いですが、より柔軟なプログラムが可能になります。 この柔軟性は、メタプログラミング機能、および実行時にプログラムの構造を変更するアフォーダンスによってもたらされます。

コンパイルされたプログラムは、多くの最適化処理がその堅牢性を利用するため、逐次実行プログラムよりも堅牢な構造を必要とします。 結果として、アフォーダンスシステムおよびプログラム構造上で動作する機能は、コンパイルされたプログラムではその機能が制限されます。

パフォーマンスが最大の関心事である場合はコンパイラを使用するべきですが、プログラマがCX機能によって提供されるすべての柔軟性を必要とする場合は、プログラムを逐次実行プログラムのままにしておく必要があります。 以下のサブセクションでは、これらの機能の一部をチュートリアルとして提供するのではなく、単なる紹介として提示します。

Read-Eval-Print Loop

Read-Eval-Print Loop (REPL) は、プログラマが新しいプログラム要素を入力し評価することができる対話的なツールです。 新しいREPLセッションを開始すると、次のメッセージがコンソールに出力されます。

CX REPL
More information about CX is available at https://github.com/skycoin/cx

*

“*“は、REPLが新しいコード行を受け取る準備ができていることをプログラマーに伝えます。 REPLはセミコロンと改行文字が出現するまでユーザーからの入力を読み続けます。

REPLに最初にロードされたプログラムがない場合、CXは空のプログラムから開始します。 これは、メタプログラミングコマンド:dProgram true;が入力として与えられている場合に見ることができます。

* :dProgram true;
Program

*

REPLは “Program"という単語を続けて表示し、その後に空の行を表示します。 最初のステップとして、新しいモジュールと関数を宣言することができます。:

最初のステップとして、新しいmain モジュールと新しいmain関数を宣言する必要があります。

* package main;
Program
0.- Module: main

* func main () () {};
Program
0.- Module: main
	Functions
		0.- Function: main () ()

*

見て分かるように、新しい要素がプログラムに追加されるたびにプログラム構造が表示されています。

メタプログラミングコマンド

上記のサブセクションで:dProgramが使用されました。 コロン(:)で始まる文は、「メタプログラミングコマンド」と呼ばれる命令カテゴリの一部です。

REPLの要素を宣言すると、CXのプログラム構造に追加されます。 しかし、他の多くのプログラミング言語と同様に、これらの宣言は追加されただけで、たいていは再定義されます。

しかし、REPLを提供する他の多くのプログラミング言語のように、プログラマはプログラムに新しい要素を追加することに制限があり、多くの場合、要素を再定義しています。 メタプログラミングコマンドを使用すると、プログラマーは、プログラムの構造がどのように変更されているか、より詳細に制御できます。

:dProgram:dState:dStackはそれぞれ、ユーザーにプログラムの構造、現在の呼び出しの状態、および完全なコールスタックを表示することによって、デバッグ目的にのみ使用されます。 :step は、インタプリタに実行の前進または後退を指示します。 :package:func:structは、selectorsとして知られており、プログラムのスコープを変更するために使用されます。 :remは、プログラムの構造から要素を選択的に削除するために使用できるremoversへのアクセスをプログラマに提供します。 :affは、CXのアフォーダンスシステムにアクセスするために使用されます。このメタプログラミングコマンドは、プログラムのさまざまな要素のためのアフォーダンスを照会および適用するために使用されます。 :clausesユーザー定義制約システムで使われるモジュールの節を設定するために使用されます。 :object:objectsは、それぞれのオブジェクトを追加し、表示するために使用されます。 :queryは、モジュールのクエリを設定するために使用され、:dQueryはユーザー定義の制約をデバッグするヘルパーです。

ステッピング

REPLモードで起動されたプログラムは、ソースファイルで定義されたプログラム構造で初期化することができます。 たとえば、次のコマンドは

$ ./cx --load examples/looping.cx

サンプルディレクトリからlooping.cxを読み込みます(全てのサンプルのリストはプロジェクトリポジトリにあります). プログラムはロードされても、まだ実行されていません。 REPLでは、プログラムを実行するためにメタプログラミングコマンド:stepを使用する必要があります。 最後までプログラムを実行するには、:step 0;を使用する必要があります。 しかし、:step は他の整数を引数(負の整数さえ)として取ることができるため興味深いです。 例えば

CX REPL
More information about CX is available at https://github.com/skycoin/cx

* :dStack false;

* :step 5;
0

* :step 5;
1

* :step 5;
2

*

examples/looping.cxのプログラムは一度に5つのステップを実行しています。 プログラムがwhile条件を再評価し、カウンタを出力し、カウンタに1を加えるために5つのステップが必要であることがわかります。

同様に、REPLに:step -5が指示されている場合は、「時間を戻す」ことになります。

...

* :step 5;
2

* :step -5;

* :step 5;
2

*

CXに5ステップを進めるように指示した後、2が再びコンソールに表示されます。 カウンタには異なる値が割り当てられているわけでは無いので、注意する必要があります。 起こっていることは、コールスタックが以前の状態に戻っているということです。

対話的なデバッグ

エラーが検出されると、CXプログラムはREPLモードに入ります。 この動作により、プログラムの実行を再開しようとする前に、プログラムをデバッグする機会がプログラマに与えられます。

以下の例では、0による除算が発生し、REPLはエラーについてプログラマに警告し、コールスタックの最後の呼び出しをダンプし、REPLはその実行を継続します。

CX REPL
More information about CX is available at https://github.com/skycoin/cx

* package main;

* func main () () {};

* :func main;
main
:func main {...
	* foo := divI32(5, 3);
main
:func main {...
	* bar := divI32(10, 0);
main
:func main {...
	* :step 0;
fn:main ln:0, 	locals:
>> 1
fn:main ln:1, 	locals: foo: 1

Call's State:
foo:		1

divI32() Arguments:
0: 10
1: 0

0: divI32: Division by 0
main
:func main {...
	*

同様に、プログラムがCXインタプリタへの入力として与えられ、REPLを呼び出さずにエラーが発生した場合、プログラマまたはシステム管理者がプログラムをデバッグするためにREPLが呼び出されます。

$ ./cx examples/program-halt.cx
1

Call's State:
nonAssign_0:		1
nonAssign_1:		1

divI32() Arguments:
0: 5
1: 0

5: divI32: Division by 0
CX REPL
More information about CX is available at https://github.com/skycoin/cx

*

統合された進化的アルゴリズム

CXのアフォーダンスシステムとメタプログラミング機能は、管理された方法でプログラムの構造を変更する柔軟性を実現します。 しかしながら、アフォーダンスは、適用されるアフォーダンスのインデックスを選択する機能を持つことによって、自動化することができます。

evolveはランダムなアフォーダンスを使用してユーザー定義関数を構築するネイティブ関数です。 対話的なプロセスがテストのために使用されます。

evolve は進化的計算の原則に従います。 特に、evolve は遺伝的プログラミングと呼ばれる技術を実行します。 遺伝的プログラミングは、問題を解決する演算子と引数の組み合わせを見つけることを試みます。 たとえばevolveに引数として10を送信すると20を返す演算子の組み合わせを見つけるよう指示することができます。 これは些細なことかもしれませんが、遺伝的プログラミングやその他の進化的アルゴリズムは非常に複雑な問題を解決できます。

リポジトリのexamplesディレクトリに、カーブフィッティング機能を進化的に処理する例 (examples/evolving-a-function.cx)があります。

シリアライゼーション

CXのプログラムは、バイト配列に部分的または完全にシリアライズすることができます。 このシリアライゼーション機能により、プログラムはプログラムイメージ(システムイメージに類似)を作成することができ 、プログラムがシリアライズされた正確な状態が維持されます。 これは、シリアライズされたプログラムをデシリアライズして後でその実行を再開できることを意味します。 シリアライゼーションを使用してバックアップを作成することもできます。

CXプログラムは、統合された機能を活用して面白いシナリオを作成することができます。 たとえば、プログラムをシリアライズして自分自身のバックアップを作成し、その機能の1つで進化的アルゴリズム を開始することができます。 進化的アルゴリズムが以前の定義よりも優れた機能を発見した場合、この新しいバージョンのプログラムを保持することができます。 しかし、進化的アルゴリズムの実行結果が良くなかった場合、プログラムは保存されたバックアップに復元することができます。 これらのタスクはすべて自動化できます。