はじめに
今回はCurl言語のデータ型について説明します。Curlは変数に型指定が必要な言語です。厳密な型指定を行いながら、一方でとても柔軟なデータ型の利用が可能になっています。Curl言語で使用するデータ型と、データ型を構築する仕組みであるTypeクラスについて見ていきましょう。
これまでのTips
データ型
Curlのデータ型は、プリミティブ型、クラス型、プロシージャ型、列挙型、null型、void型、any型の7種類があります。すべてのデータ型はオブジェクトとして扱うことができます。それでは、1つ1つのデータ型の特徴を説明します。
プリミティブ型
プリミティブ型は、文字、数値、論理値など基本的なデータを表します。非常にシンプルな構成をとっており、Curl言語のコアに直接実装されています。そのため処理が高速です。
プリミティブ型はデータそのものと考えられ、メソッドはありません。規定値(変数を宣言した時点での値)を持ち、null値をとることができません。プリミティブ型には以下のものがあります。
符号付き整数型(int、 int8、 int16、 int32、 int64)
符号付きの整数、プラスとマイナスの両方の数字を扱えます。int(integer) にデータを表現するために使用するビット数を追加して型を表します。int型はint32型と同じ型になります。規定値はすべて0です。
符号無し整数型(uint、 uint8、 uint16、 uint32、 uint64)
符号無しの整数、プラスの数字のみを扱えます。マイナスの値をとりませんので、ビット毎のフラグに使ったり、符号付整数型よりも大きな正の数を扱う場合に使います。int と同じように、unit (unsigned integer)にデータを表現するために使用するビット数を追加して型を表します。uint型はuint32型と同じ型になります。規定値はすべて0です。
浮動小数点型(float、 double)
符号付きの実数を扱います。数値を持つ精度と値の表す範囲によって、float(32bit)、 double(64bit) の指定があります。規定値はすべて0.0です。浮動小数点型には、正の無限大を表す infinity、 負の無限大を表す -infinity、 数値でない値を表す nan (not a number) があります。
ブール値(bool)
true か false かを表す論理値を扱います。規定値はfalseです。1bitの大きさです。
文字(char)
文字を扱います。文字はunicodeで保持されます。1文字のコードを表しています。文字列ではありません。規定値は、文字コード 0 です。
数量(deg、 rad、 px、 m、 in、 ...)
Curl では「数量」と呼ばれる単位を含めた数値をプリミティブ型の変数として定義できます。三角関数などを利用する場合は、角度の単位として値を指定する必要があります。数量はSI単位系に変換されて、float または double 値として内部的に保持されます。測定単位は同じですが、精度が異なります。基本的には、double型を利用する事を推奨しています。単位には、G(ギガ)、M(メガ)、K(キロ)、m(ミリ)などのプレフィックスと組み合わせて利用できます。
クラス型
オブジェクト指向の中心となるクラスを表します。define-class で定義されます。
Curlでプログラムを書くということは、多くの場合クラスの実装になります。プリミティブ型との大きな違いは、クラスはデータとメソッド(振る舞い)の両方を持つことです。また、多くのクラスは、ひな形(クラス定義)から、インスタンスを生成して使用します。
クラスについて説明をするのは、ほぼオブジェクト指向言語の説明になってしまいますので、ここでは、Curlの特徴的な部分についてのみ触れておきます。
多重継承
最近のオブジェクト指向言語の多くが1つのクラスのみを継承できる単一継承ですが、Curlは複数のクラスを継承できる多重継承をサポートしています。
パラメータ化クラス
クラス設計をするときには取り扱える型を指定せず、プログラマの使いたい型を指定して利用するクラスをパラメータクラスと言います。コレクションフレームワークなどで威力を発揮します。Javaではジェネリックスと呼ばれています。
Objectクラスを暗黙の継承とする
多重継承ではありますが、必ずObjectクラスを継承します。メソッドのオーバーライドは可能ですが、オーバーロードはサポートしていません。
プロシージャ型
プリミティブ型とは反対に、データを持たずに、メソッドだけを記述するのがプロシージャです。define-proc で定義されます。プロシージャ型もオブジェクトです。配列やハッシュテーブルに入れることができます。一般的にはクロージャと呼ばれています。Curlの特徴的な点は以下のとおりです。
複数の引数指定方法を持つ
位置パラメータ、キー付きパラメータ、残余パラメータと豊富な引数指定方法を持ちます。
複数の値の戻り値を返せる
戻り値を複数返すことができます。ポインタを使わない1つの値のみしか返せない言語では、戻り値用のクラスを作ったりしなければなりませんが、Curlは簡単に複数の値を返せます。
列挙型
列挙型は、define-enum で定義される指定された要素の固定リストです。列挙型は、クラスのようなメソッドの追加ができません。要素名を設定することと、簡単な値(数値、char、bool、String)を設定することが可能です。列挙型を使うことで、定数に型を持たせることができますので、パラメータなどで使用可能な値の制限を簡単に実現できます。
null型
null は、拡張クラス型(#型名で表現されるnull値を許すクラス型)またはプロシージャ型の変数で利用できる「なにもない」状態を表します。null をプリミティブ型の変数 (数量など)、または通常の非nullクラス型の変数に代入することはできません。
拡張クラス型またはプロシージャ型の変数に null 値が設定されている場合は、変数に現在オブジェクトがないことを表しています。変数にオブジェクトを代入すると、この変数は null 値ではなくなります。null が許されているデータ型の変数に、null値を明示的に代入することもできます。
void型
値を返さないプロシージャまたはメソッドの戻り値のデータ型となります。クラスのメソッドなどの戻り値として頻繁に指定しますが、この型をプログラム処理で利用することはあまり無いと思われます。
any型
どのようなデータの処理をするかわからないような場合、すべての型に対応するのが、any 型です。any 型の変数には、プリミティブ型、クラス型、プロシージャ型などのすべてのデータ型の値を代入できます。null値を代入することもできます。any型を利用すると、Curlの実行環境への負荷が重くなります。最低限の使用にとどめましょう。
Typeクラス
以上でデータ型の説明は終わりです。次は、これらのデータ型をささえる仕組みについて見ていきます。
Curl言語が優れているのは、データ型をプログラムで容易に扱え、処理に一貫性がある点です。今まで説明してきたすべての型は、すべてTypeクラスのサブクラスの「インスタンス」になります。Typeクラスのサブクラスではないので注意してください。
Typeクラスの構成は、図1、表1のようになります。
サブクラス | 対応する型 | 具体的なインスタンス |
---|---|---|
NumericType | プリミティブ型 | int, uint, double, bool など |
ClassType | クラス型 | Object, String, Dateなど全ての define-class で定義したクラス |
ProcType | プロシージャ型 | 全ての define-procで定義したプロシージャ |
NullType | null型 | nullのみ |
EnumType | 列挙型 | 全ての define-enumで定義した列挙 |
AnyType | any型 | anyのみ |
VoidType | void型 | voidのみ |
具体的に説明しますと、int型はTypeクラスのサブクラスであるNumericTypeクラスのインスタンスになります。すべてのプリミティブ型はこのNumericTypeクラスのインスタンスとして定義されます。
同様に、define-class で定義されるすべてのクラス型はTypeクラスのサブクラスであるClassTypeクラスのインスタンスになります。
これらのTypeクラス、Typeクラスのサブクラスを使う事で、強力なリフレクションの機能を実現しています。「すべてのデータ型はTypeクラスの(サブクラスの)インスタンスになる」この意味が分かれば、応用範囲はとても広くなります。
type-ofマクロ
変数(インスタンス)からデータ型を得るために、type-of マクロが用意されています。マクロの中でも比較的わかりやすいもので、type-of の引数として変数を設定するだけで利用できます。
すべてのデータ型は、Typeクラスのインスタンスですので、各データ型を type-ofマクロの引数にした例を以下に示します。実行結果を見やすくするために、サンプルコードがやや読みにくくなっていますがご了承ください。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || 列挙型用定義 {define-enum Car sedan, rv, coupe } || Table作成 || {Table columns = 3, border-width=1px, border-color="gray", cell-border-width=1px, cell-border-style="sunken", cell-border-color="gray", || {row-prototype "void型", "void", {type-of void}}, || {row-prototype "any型", "any", {type-of any}}, || {row-prototype "null型", "null", {type-of null}}, || {row-prototype {cell-prototype "プリミティブ型", rowspan=6}, {cell-prototype "int"}, {cell-prototype {type-of int}} }, {row-prototype {skip 1}, "uint", {type-of uint}}, {row-prototype {skip 1}, "byte", {type-of byte}}, {row-prototype {skip 1}, "bool", {type-of bool}}, {row-prototype {skip 1}, "char", {type-of char}}, {row-prototype {skip 1}, "Angle", {type-of Angle}}, || {row-prototype {cell-prototype "クラス型", rowspan=4}, {cell-prototype "Object"}, {cell-prototype {type-of Object}} }, {row-prototype {skip 1}, "Type", {type-of Type}}, {row-prototype {skip 1}, "DateTime", {type-of DateTime}}, {row-prototype {skip 1}, "String", {type-of String}}, || {row-prototype "ProcType型", "proc-type", {type-of {proc-type {any}:int}} }, || {row-prototype "列挙型", "Car", {type-of Car}} }
実行結果です(図2)。
いくつか興味深い結果がでています。nullのタイプが NullType とならずに Null となっています。そして、String が StringType という独自のクラスタイプで定義されている点です。StringTypeは、リファレンスにはでてきません。想像ですが、Curl実行環境が文字列(StringInterface)について特別な処理をしているので、特別扱いしていると思われます。もちろん、StringType は ClassType を継承しています。
isa演算子・asa演算子
ここからは、型を使ったより実践的な処理について説明します。any変数を引数で利用した場合などデータ型を判定し、その種類によって処理を分けたい場合について考えます。
今、複数の引数の平均値を計算するプロシージャaverageを考えます。処理を単純化するために、引数には整数(int)、実数(double)、文字列(String)のどれかが渡されるとします。引数の型をチェックするために、if式の条件判断にisa演算子を使います。変数をanyから特定の型にキャストするために、asa演算子を使います。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || if を使ったプロシージャ || {define-proc public {average ...:any}:double let sum:double = 0 let count:int = 0 {for data:any in ... do {if data isa int then let i-data:int = data asa int {inc sum, i-data} elseif data isa double then let d-data:double = data asa double {inc sum, d-data} elseif data isa String then let s-data:String = data asa String {inc sum, {s-data.to-double}} else {error {format "無効なデータ:%s", data}} } {inc count} } {return {if count > 0 then sum / count else 0.0}} } average = {average 4, 6, 2.3, 4, "12", "5.2", 1, 3.1, "12.4", 1.9}
実行結果です(図3)。
switch式(マクロ)
今度は、先ほどのサンプルプログラムをswitch式で書いてみます。switch式では、switch の後に書かれた変数と case で指定された値と比較を行う演算子の指定ができます。今回は、isa演算子を使った比較を行いたいので、switch の後に、using isa を指定しています。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || switch ... using isa を使ったプロシージャ || {define-proc public {average ...:any}:double let sum:double = 0 let count:int = 0 {for data:any in ... do {switch data using isa case int do let i-data:int = data asa int {inc sum, i-data} case double do let d-data:double = data asa double {inc sum, d-data} case String do let s-data:String = data asa String {inc sum, {s-data.to-double}} else {error {format "無効なデータ:%s", data}} } {inc count} } {return {if count > 0 then sum / count else 0.0}} } average = {average 4, 6, 2.3, 4, "12", "5.2", 1, 3.1, "12.4", 1.9}
type-switch式(マクロ)
switch式では、using isa で比較に使用する演算子を指定して、その値を asa演算子でキャストして利用しました。その2つを同時に行ってくれるのが、type-switch式になります。type-switch式でサンプルプログラムを書き直してみます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || type-switch を使ったプロシージャ || {define-proc public {average ...:any}:double let sum:double = 0 let count:int = 0 {for data:any in ... do {type-switch data case i-data:int do {inc sum, i-data} case d-data:double do {inc sum, d-data} case s-data:String do {inc sum, {s-data.to-double}} else {error {format "無効なデータ:%s", data}} } {inc count} } {return {if count > 0 then sum / count else 0.0}} } average = {average 4, 6, 2.3, 4, "12", "5.2", 1, 3.1, "12.4", 1.9}
switch の内部がとてもすっきりしました。
最後に
Curl言語の中枢にあるデータ型について見てきました。プリミティブ型、クラス型、プロシージャ型などがあり、すべてオブジェクトとして扱うことが可能です。データ型のすべてがTypeクラスのサブクラスのインスタンスとして関連付けられていました。Typeクラスおよびそのサブクラスを利用することで、強力なリフレクション機能を実現できます。
最後にデータ型の判定方法をサンプルプログラムを通して説明しました。データ型は簡単そうに見えて、奥が深い世界です。いろいろ試してみてください。