Amber

A Language for High-Level Programming with Self-Extension

チュートリアル4: オブジェクトの構造

前章では組み込みのオブジェクトについて説明しましたが、本章ではAmberの一般的なデータ構造について説明します。

オブジェクトの構造

Amberの全てのオブジェクトは形式的には

  • 1つのシンボル(ヘッド)
  • 0個以上のオブジェクト(アーギュメント)

からなります。これをヘッドをH、アーギュメントをa0, a1, …, a(n-1)として

H{a0, a1, ..., a(n-1)}

と表すことにします。

全てのデータは何らかの概念を普通は表しているわけですが、ヘッドHにはその概念を表す名前を与えます。 そして、アーギュメントにはその概念を構成する要素を与えます。いくつか例を考えてみましょう。

例えば2次元の「点」という概念は数値x,yを用いて

Point{x, y}

の様に表す事が出来るでしょう。すると2点point1,point2を通る「直線」は

Line{point1, point2}

のように表せると思います。そして2直線line1,line2が「平行である」という概念は

Parallel{line1, line2}

と表せるでしょう。

別の例を考えてみます。例えばxyの「和」という概念は

Add{x, y}

と表す事が出来ます。また関数fを引数a,b,cに「適用する」という概念は

Apply{f, [a, b, c]}

と表す事が出来るでしょう([a,b,c]はListオブジェクトです)。このようにしてAmberではプログラム自体もデータも区別せずに同一の形式で取り扱います。

さて、Amberのオブジェクトは一部の例外を除き、ヘッド・アーギュメント以外の内部構造を持ちません。例えばオブジェクト指向言語におけるオブジェクトは「クラス」「インスタンス変数」「メソッド」などの構造を持っていますが、Amberのオブジェクトはシンボルといくつかのオブジェクトのに過ぎません。 また、データ構造の宣言も不要であり任意のシンボルとアーギュメントからオブジェクトを構築することが可能です。オブジェクトのヘッドは唯のシンボルであり、クラスや型といった特定の概念と結びついているものではありません。

Amberがこのようなデータ構造を利用しているのは、柔軟なメタプログラミングを可能にする為には全てのデータがシンプルな正規形を持つことが重要であるからです。この事はLISP系言語の能力をみれば明らかでありAmberもLISPの思想の影響を受けています。一方で、全てのデータをドット対によって構成するというLISPの方法は数学的な視点を外して見てみれば大変不自然な概念の表現方法だと感じています。特にデータの表す概念の名前を指定する標準的な方法が無いのが問題であると思っています。Amberがヘッドを持つデータ構造を採用しているのはその為です。この点についてはいずれページを割いて詳しく考察を述べようと思います。

基本的なオブジェクトの構造

さて前章で紹介した組み込みのオブジェクトがどのようになっているかを見てみましょう。そのためにはfullformという関数を利用出来ます。この関数は任意のデータを、上で述べた形式の文字列表現に変換する関数です。まずコンテナから見てみると以下のようになっています。

amber:1> fullform([1,2,3])
=> "List{1, 2, 3}"
amber:2> fullform(Array{1,2,3})
=> "Array{1, 2, 3}"
amber:3> fullform((1,2,3))
=> "Tuple{1, 2, 3}"

それぞれのヘッドは

  • リスト: List
  • 配列: Array
  • タプル: Tuple

というシンボルになっていて、アーギュメントは各要素となっています。次に連想配列を見てみると次のようになります。

amber:1> fullform(Table{"one" => 1, "two" => 2})
=> "Table{...}"

連想配列のヘッドがTableであるということは判りますが、アーギュメントが...という表記になっています。 連想配列は効率の為に特殊な実装を行なっており、アーギュメントを直接読み書きしてもらいたくない為にこの表な表示になっています。

同様にしてアトムも調べてみると以下のようになります。

amber:1> fullform("hello")
=> "String{...}"
amber:2> fullform(\hello)
=> "Symbol{...}"
amber:3> fullform(123)
=> "Int{...}"
amber:4> fullform(1.5)
=> "Float{...}"

それぞれのヘッドは

  • 文字列:String
  • シンボル:Symbol
  • 整数:Int
  • 浮動小数点数:Float

というシンボルになっていて、アーギュメントは...となっています。

このように一部の組み込みのオブジェクトはアーギュメントを直接読み書き出来ないようになっています。 これらのオブジェクトはそれぞれ専用の組み込み関数を用いて処理する事になります。

ヘッド・アーギュメントの取り出し

ヘッドを取り出すには関数headを利用する事が出来ます。

amber:1> head("Hello")
=> String
amber:2> head(1)
=> Int
amber:3> head([1, 2, 3])
=> List

アーギュメントを取り出すには関数argumentsを利用して下さい。 この関数はアーギュメントのリストを返します。

amber:1> arguments([1,2,3])
=> [1, 2, 3]
amber:2> arguments(Array{1,2,3})
=> [1, 2, 3]
amber:3> arguments((1,2,3))
=> [1, 2, 3]

またアトムなどのアーギュメントを読み書き出来ないオブジェクトでは空のリストが返ります。

amber:1> arguments("hello")
=> []
amber:2> arguments(\hello)
=> []
amber:3> arguments(1)
=> []
amber:4> arguments(1.5)
=> []
amber:5> arguments(Table{"one" => 1, "two" => 2})
=> []

オブジェクトの生成

H{a0, a1, ..., a(n-1)}

の形式で記述すればオブジェクトが生成されます。例えば以下のようになります。

amber:1> List{1, 2, 3}
=> [1, 2, 3]

[...]という構文は実はList{...}の構文糖衣であるわけです。

他のオブジェクトも同じ構文で入力出来ますが、Amberの処理系は入力されたオブジェクトをプログラムとして解釈しようとするので、プログラムとして意味を持たないオブジェクトを直に入力すると下のようにエラーになります。1

amber:1> SomeObject{1}
Error: UnknownExpression{[amber:1], SomeObject{1}}

そこでプログラムではなくオブジェクト自体を入力したい場合には、クォーテーションを利用して以下のように記述します。

amber:1> \SomeObject{1}
=> SomeObject{1}

オブジェクトの一部だけを評価したい場合にはバッククォート`による準クォートを用います。 記号!の箇所だけ評価(アンクォート)が行われます。

amber:1> x: 0
=> 0
amber:2> `SomeObject{1, !x}
=> SomeObject{1, 0}

クォーテーションについて詳しくは後の回に紹介します。

また、make式という構文もあります。これは下の様に引数を全て評価した上でオブジェクトを生成します。make式についても後の回に詳しく説明します。

amber:1> make SomeObject{1, 2 + 3}
=> SomeObject{1, 5}

  • [1] List{...}などのオブジェクトは、そのデータ自身を返すプログラム(リテラル式)として処理系が解釈するので直に入力が可能です。