ICI言語を使おう
2012/7/1 aito
Cによく似たスクリプト言語
ICIは、1992年にTim
Longが作ったスクリプト言語です。あまり注目されていませんが、もっと使われても良い言語だと思うので、紹介文を書いてみました。
ICIはスクリプト言語なので、perl, ruby, pythonあたりと同じような用途に使うことができます。すなわち、
- データを処理するアドホックなプログラムを素早く書く
- そこそこの規模だが高速性はそれほど必要でないプログラムを楽に書く
といったことに使えます。
「じゃあ別に ruby とかでいいんじゃね?」と思わないでもないですが、ICIの最大の特徴は、C言語(C++ではない)によく似
ていることです。プログラミング言語を一つだけ知っている大学生などは「Cしか知らない」ことが圧倒的に多い
ので、そういう人たちが使うのに適した言語だと思います。
ICIの文法はCに似ていますが、スクリプト言語に必要な機能を一通り持っています。たとえば、
- 動的な型付け
- 動的な配列と連想配列
- 正規表現
- 関数のオブジェクト化
- GC
などです。
準備とプログラム実行
ICIインタプリタはSourceForge
のサイトからダウンロードできます。WindowsとLinux用のバイナリが用意されていますが、ソースプログラムからビルド
するとMacOSでも動くようです。
ICIインタプリタのコマンド名は ici です。適当なスクリプト prog.ici について、
ici prog.ici
を実行することでスクリプトを実行することができます。
インストールするとPDFのマニュアルがついてきます。オンラインマニュアルもありま
すが、どうやら不完全なようです。ちなみにマニュアルは英語版しかありません。
プログラム例:Cとの違い
入力の各行に整数が書いてあって、その総和と平均を出力するプログラムはこんな感じです。
/* 総和と平均 */
auto sum = 0;
auto n = 0;
while ((line = getline()) != NULL) {
sum += int(line);
n++;
}
auto avg = float(sum)/n; // 途中で変数宣言しても良い
printf("N=%d\tSum=%d\tAverage=%f\n",n,sum,avg);
|
Cみたいにプログラムを書きたい場合に、気をつけることは次のようなところです。
- 変数には型がない。変数宣言はしなくてもよいですが、する場合は static か auto
を使います。staticはCのstaticと同じ意味です。特に指定しなければ auto になります。
- 変数にはどんな型のデータ(整数、実数、文字列など)でも代入できます。
コメントは(最近の)Cと同じで、/* コメント */ か、 // コメント(行末まで) のように書きます。
プログラム例:配列
次に、入力の各行に実数が書いてある時、その平均と分散を(ナイーブな方法で)計算するプログラムを見てみましょう。
/* 統計分析 */
data = [array];
while ((line =
getline()) != NULL) {
push(data,float(line));
}
n = nels(data);
sum = 0.0;
for (i = 0; i < n;
i++) {
sum += data[i];
}
mean = sum/n;
sum2 = 0.0;
for (i = 0; i < n;
i++) {
sum2 =
pow(data[i]-mean,2);
}
var = sum2/n;
printf("N=%d\tMean=%g\tVariance=%g\n",n,mean,var);
|
ICIには「配列変数」というものはなく、通常の変数に「配列」を代入します。
data = [array];
最初から値の入っている配列を代入することもできます。
data = [array 1, 2, 3];
配列には異なる型のデータを混在させることができます。
data = [array 1, "hello", 3.1415];
配列は関数っぽく書くこともできます。
data = array(1, "hello", 3.1415);
配列を [array 式]
と書いた時には式はプログラム読み込み時に評価され、array(式)と書いた時には実行時に評価されます。簡単な覚え方としては、[array
...] は定数で初期化するときに使い、プログラム内で動的に配列を作るときには array(...) を使うといいでしょう。
配列へのアクセスはCと同じですが、配列に上限はなく(下限は0)、配列サイズは必要に応じて大きくなります。また、次のような便利な関数/手続
きが使えます。
- push(配列, 要素) 配列の最後に要素を付け加える。
- x =
pop(配列)
配列の最後の要素を取り出し、配列サイズを1つ小さくする。
- rpush(配列, 要素) 配列の最初に要素を付け加える。
- x = rpop(配列)
配列の最初の要素を取り出し、配列サイズを1つ小さくする。
- n = nels(配列) 配列のサイズを返す。(Number of ELementS)
プログラム例:構造体と連想配列(ハッシュ)
ICIでは、Cの「構造体」のようなものも使えます。ICIの構造体は、実はperlなどにある「連想配列」(ハッシュ)と同じものです。例とし
て、英語の文章を入力すると単語の出現頻度を数えるプログラムを見てみましょう。
/* 単語出現頻度カウント */
count = [struct];
words = gettokens();
forall (w in words) {
if (count[w] == NULL)
count[w] = 1;
else
count[w]++;
}
forall (n,w in count) {
printf("%s %d\n",w,n);
}
|
[struct]は連想配列(構造体)です。配列のときと同じように「構造体変数」というものはなく、普通の変数に連想配列を代入することで連想
配列として使えるようになります。連想配列では、配列のキーとして文字列を使うことができます。
gettokens()関数は、入力をスペースで区切られたトークン列に分解して、それをまとめて1つの配列として返します。便利です。
この中に出てくる forall という文は、配列と連想配列の各要素に順番にアクセスするための制御文です。配列 a
の全要素にアクセスする場合は
forall (x in a) { ... }
同じく連想配列にアクセスする場合は
forall (value, key in a) { ... }
と書けます。
※forall で連想配列にアクセスする場合、key と value
の順番が perl や ruby と逆になっているので、これらの言語になれている場合には注意が必要です。
ICIのstructは、Cの構造体のように書くこともできます。次の3通りの書き方は、全く同じものを指します。
- count["abc"]
- count.abc
- count.("abc")
初期値を持った構造体は、
x = [struct name = "Mike Smith", age = 24];
のように書くことができます。配列の時と同じように
x = struct("name", "Mike Smith", "age", 24);
のようにも書けます。前者は定数による初期化、後者は実行時に構造体を作る書き方です。
プログラム例:関数定義
/* 関数の例 */
static factorial(n)
{
if (n == 0)
return 1;
return
n*factorial(n-1);
}
printf("%d\n",factorial(10));
|
関数定義はCとよく似ています。ただし、違う点は次のとおりです。
- 関数の返す値の型を宣言する必要がないので、型にあたる部分には static と書きます。
- 引数リストには型を書く必要がありません。
- Cのプロトタイプ宣言に当たるものはないので、関数定義は必ず関数を使う前にする必要があります。
プログラム例:関数と変数
/* 関数と変数の例 */
static foo()
{
x = 10;
auto y = 20;
printf("x=%d, y=%d\n",x,y);
}
static x = 100;
static y = 200;
foo();
printf("x=%d, y=%d\n",x,y);
|
このプログラムを実行すると、
x=10, y=20
x=10, y=200
と表示されます。
グローバル変数とローカル変数の話です。関数の外側で「auto 変数」または宣言なしに使う変数は auto
変数で、これを関数の中から参照することはできません。いわゆるグローバル変数を使う場合は「static 変数」という宣言が必要です。
static 宣言された変数は関数の中からアクセスできます。関数の中でローカル変数を使いたい場合には、auto
宣言をする必要があります。または、
x := 10;
と記述すると、xをローカル変数として値を代入することができます。
プログラム例:正規表現
ICIでも正規表現が使えます。正規表現は #...# と書くので、perlなどとはちょっと雰囲気が違いますが、中身の記述は同じです。
入力に "Name: 名前 Age: 年齢" という文字列が来た時に、名前と年齢の一覧を作るプログラムです。
/* 正規表現 */
namelist = [array];
auto a;
while ((line =
getline()) != NULL) {
if ((a = line
~~~ #^Name:\s*(.+)\s*Age:\s*(\d+)#) != NULL) {
push(namelist,struct("name",a[0], "age",a[1]));
}
}
forall (x in namelist)
{
printf("name=%s
age=%s\n",x.name, x.age);
}
|
#^Name:\s*(.+)\s*Age:\s*(\d+)#
の部分が正規表現です。正規表現の書き方自体は他の言語とほとんど同じです。正規表現を使った演算子や関数として、次のようなものが使えます
(全部ではありません)。
- 式 ~ 正規表現 : 式が正規表現とマッチするときには true、しないときには false を返します。
- 式 !~ 正規表現 : 式が正規表現とマッチするときには false、しないときには true を返します。
- 式 ~~ 正規表現 : 正規表現の中に1個だけ ( )
が含まれる必要があります。式が正規表現にマッチするときには、(
)の中にマッチした文字列を返します。マッチしないときにはNULLを返します。
- 式 ~~~ 正規表現 : 正規表現の中に ( )
が1つ以上含まれる必要があります。式が正規表現にマッチするときには、(
)の中にマッチした文字列の配列を返します。マッチしないときにはNULLを返します。
- sub(文字列1, 正規表現,
文字列2) : 文字列1の中で最初に正規表現にマッチした部分を文字列2に置換した文字列を返します。
- gsub(文字列1, 正規表現,
文字列2) : 文字列1の中で正規表現にマッチした部分をすべて文字列2に置換した文字列を返します。
- regexp(文字列) : 文字列から正規表現を生成します。大文字小文字を区別したくない場合は
regexpi()を使います。
- smash(文字列1, 正規表現, 文字列2,
フラグ) : 文字列1を正規表現でスキャンして、各マッチした部分ごとに文字列2で置き換えたものを並べた配列を返します。フラグが0以外だと、最後に
残った文字列を配列の最後に加えます。この関数の一般的な使い方は、perl の split
のように文字列を区切り文字で分割して配列にすることで、この場合は
smash(文字列, 区切りの正規表現, 1)
と書きます。例えば、
smash("ab cd efg",#\s+#,1)
は、[array "ab" "cd" "efg"] と同じものを返します。正規表現を省略すると、文字列を改行で分割します。
ちなみに、タブやスペースなどの固定文字で文字列を分割するなら、 gettokens()を使うこともできます。
プログラム例:ファイル入出力
ファイルの入出力はCと似ています。以下のプログラムは、コマンドライン引数に2つのファイル名を書くと、1番目のファイルを2番目のファイルに
コピーします。
if (nels(argv) != 3) {
printf(stderr,"Usage: %s from-file to-file\n");
exit(1);
}
f1 = fopen(argv[1],"r");
f2 = fopen(argv[2],"w");
while ((line = getchar(f1)) != NULL) {
put(line,f2);
}
close(f1);
close(f2);
|
まず本題のファイル入出力から言うと、fopen() がファイルを開く関数で close() が閉じる関数です。閉じるほうが
fclose() でないところがいまいちです。使い方はほとんどCと同じですので、説明の必要はないでしょう。
この例題ではコマンドライン引数を使っています。コマンドライン引数は配列 argv に格納されています(配列自体から長さを nels()
で取り出せるので、argc はありません)。argv[0] には常にスクリプト名が入ります。引数は argv[1] 以降です。
ファイル入出力には、例えば次のような関数があります。
- getchar(file) : ファイルから1文字読んで、文字列として返します。読めなかったらNULLを返します。
- getline(file) : ファイルから1行読んで以下同文。
- getfile(file) : ファイルの中身を全部読んでまとめて文字列として返します。前述のsmash()と組み合わせて
ary = smash(getfile(file));
とすると、ファイルの中身を行単位の配列に簡単に変換できます。
- eof(file) : ファイルの最後まで来てたら true。
- flush(file) : バッファに溜まっている出力をファイルに書き出します。
- gettoken(file, seps) : ファイルから
seps(文字列)に含まれない文字を切り出してきて、それを返します。 seps
を省略すると空白文字が仮定されるので、要するにファイルから1単語読んできて返す関数だと思えばいいです。
- gettokens(file,seps) : ファイルから
seps(文字列)に含まれない文字を切り出してきて、それを全部集めて配列にして返します。ファイルの代わりに文字列を指定することができて、この場合
は split のように使えます。本当は終端文字とかもう少し面倒な指定をすることも可能ですが、あまり使わないでしょう。
- printf([file], format,
...) : おなじみprintfですが、Cのfprintfと同じく、先頭に出力ファイルを指定することができます。
- put(文字列 [,file]) : 文字列を出力します。改行はされません。
- popen(コマンド文字列, モード) : C の popen()と同じです。
- sopen(文字列, モード) : 文字列をファイルの中身だと思ってファイルポインタを返します。読み込み専用です。
通常のファイル入出力だけでなく、ソケット通信もできます。
その他
他にもだいたい普通のスクリプト言語でやりたいと思う機能は一通り揃っていて(配列のソート、文字列操作、外部コマンドの呼び出し、等々)それな
りに便利ではあります。関数がオブジェクトだったりして、普通のスクリプト言語とちょっと違う雰囲気の部分もあります。
また、ICI4
からオブジェクト指向っぽいプログラムが書けるようになったのですが、C++ともJavaともObjective-Cとも違う雰囲気です。概念的には
JavaScriptに近いですが、記述法はだいぶ違います。