うさぎさんでもわかる並列言語Chapel
はじめに
本稿で紹介するChapelは、型推論とテンプレートと区分化大域アドレス空間を特徴とした並列言語である。 概して並列処理は、プログラムの可読性と移植性を損ね、挙動が複雑で、所期の性能を得るのは困難を伴う。 故にこそ、並列処理の詳細な実装を隠蔽し、自動的に高度な最適化を施す高生産性並列言語には価値がある。
環境の構築
Chapelのウェブサイトで頒布されているアーカイブを入手して展開し、下記のようにmake
でビルドする。
$ tar xzf chapel-1.14.0.tar.gz
$ make -C chapel-1.14.0
環境変数を設定して、コンパイラへのパスを通す。例えばbash
の場合、.bashrc
に以下の2行を追記する。
export CHPL_HOME=~/.chapel-1.14.0
cd $CHPL_HOME && source util/setchplenv.sh > /dev/null && cd - > /dev/null
最初の例題
テキストエディタで下記の通りに1行だけのプログラムを打ち込み、hello.chpl
と名前を付けて保存する。
>writeln("Hello, world!");
シェルを起動してchpl
コマンドでコンパイルする。実行可能ファイルhello
が生成されるので、起動する。
$ chpl -o hello hello.chpl
$ ./hello
Hello, world!
なお、実行可能ファイルhello
には、Chapelのコンパイラによって自動的にhelp
オプションが追加される。
$ ./hello --help
chpl
コマンドにも様々なオプションがある。例えば、savec
オプションを指定すると、C言語に変換できる。
$ chpl --savec hello hello.chpl
fast
オプションを指定すれば、推奨設定での最適化が有効になる。リリース版をビルドする際に重宝する。
$ chpl --fast -o hello hello.chpl
字句と型
識別子
大文字と小文字は区別する。
空白(0x20
)とタブ(0x09
)と改行(0x0A
)と復帰(0x0D
)は、区切り文字である。
- | - |
---|---|
識別子 | ident := [A-Z_a-z] [$0-9A-Z_a-z]* |
:
基本型
Chapelは強い静的型付けを行い、型はコンパイル時に決まる。 言語仕様に組み込まれた型を基本型と呼ぶ。
- | - |
---|---|
ボイド型 | void |
論理型 | bool |
整数型 | int uint |
実数型 | real |
虚数型 | imag |
複素数型 | complex |
文字列型 | string |
:
リテラル
- | - |
---|---|
論理値 | bool := 'true' | 'false' |
整数値 | int := ('0x' | '0X' | '0o' | '0O' | '0b' | '0B')? [0-9]+ |
実数値 | real := [0-9]* ([0-9] '.' | '.' [0-9]) [0-9]* ('e' [0-9]+)? |
虚数値 | imag := real 'i' |
文字列 | text := ('"' char* '"' | "'" char* "'") |
:
コメント
C言語(C99)と同様に、1行のコメントは//
の右側に、複数行のコメントは/*
と*/
で囲んだ中に、記述する。
>// 1-line comments
/*
multiline comments
or block comments
*/
式と演算子
評価戦略
式は正格に評価され、演算子や関数は値呼びか参照呼びである。 式が定数の場合は、積極的に部分評価する。
優先順位と結合
- | - | - | - |
---|---|---|---|
関数適用 | . () [] |
左結合 | 高 |
実体化 | new |
右結合 | |
型変換 | : |
左結合 | |
累乗算 | ** |
右結合 | |
集約演算 | reduce scan |
左結合 | |
否定演算 | ! ~ |
右結合 | |
乗除算 | * / % |
左結合 | |
単項演算 | + - |
右結合 | |
ビットシフト | << >> |
左結合 | |
ビット論理積 | & |
左結合 | |
排他的論理和 | ^ |
左結合 | |
ビット論理和 | | |
左結合 | |
加減算 | + - |
左結合 | |
レンジ | .. |
左結合 | |
順序比較 | <= => < > |
左結合 | |
等値比較 | == != |
左結合 | |
短絡論理積 | && |
左結合 | |
短絡論理和 | || |
左結合 | 低 |
:
オーバーロード
Chapelの演算子は、再定義することで新たなデータ型に対応する。 これを演算子のオーバーロードと呼ぶ。
>proc !(str: string) return str + "!";
writeln(!"Hello, World");
上の例は、オーバーロードにより文字列を引数に取る演算子!
を定義した。同様に、2項演算子も定義できる。
>proc *(str: string, n: int) return + reduce (for 1..n do str);
変数と定数
変数
変数は、破壊的代入が可能で、その型はコンパイル時に決定する。
下記は、int
型の変数foo
の宣言である。
>var foo: int;
変数宣言では、変数の初期値を指定することもできる。
下記は、int
型の変数foo
に初期値12
を代入する。
>var foo: int = 12;
初期値が明示的に与えられる場合は、型を省略できる。
下記は、int
型の変数foo
に初期値12
を代入する。
>var foo = 12;
静的定数
定数の中でも、予約語param
を前置して宣言された定数は、parameterとなる。本稿では、静的定数と呼ぶ。
静的定数の値は、基本型か列挙型で、コンパイル時に確定する。下記は、int
型定数foo
に12
を代入する。
>param foo: int = 12;
静的定数の宣言では、定数値を明示する必要上、初期化の式を省略することはできないが、型は省略できる。
>param foo = 12;
静的定数は、第 9.7節に述べる通り、ジェネリクスの基盤である。
また、定数への破壊的代入は禁止される。
なお、予約語config
を予約語param
に前置して宣言する静的定数は、定数値をコンパイル時に変更できる。
>config param foo = 121;
上記の静的定数foo
の値は、デフォルトでは121
だが、コンパイル時にset
オプションにより変更できる。
$ chpl param.chpl --set foo=12321;
動的定数
定数の中でも、予約語const
を前置して宣言された定数は、constantとなる。本稿では、動的定数と呼ぶ。
動的定数の値は、任意の型で、実行時に確定し、変更できない。下記は、int
型定数bar
に12
を代入する。
>const bar: int = 12;
動的定数の宣言では、定数値を明示する必要上、初期化の式を省略することはできないが、型は省略できる。
>const bar = 12;
動的定数は、それ自体がimmutableであることを示し、その参照先がimmutableであることは保証しない。
>class Bar {
var mutable;
}
const bar = new Bar();
bar.mutable = 114514;
なお、予約語config
を予約語const
に前置して宣言する動的定数は、値をプログラム起動時に変更できる。
>config const bar = 121;
上記の動的定数bar
の値は、デフォルトでは121
だが、定数と同名のオプションにより起動時に変更できる。
$ chpl -o const const.chpl
$ ./const --bar=12321
型の別名
定数の中でも、予約語type
を前置して宣言された定数は、type
aliasとなる。 直訳すれば、型の別名である。
>type num = int;
型の別名は、変数宣言や関数定義を始め、あらゆる場所で利用できる。
下記は、int
型定数foo
を宣言する。
>param foo: num = 121;
なお、予約語config
を予約語type
に前置して宣言する型の別名は、設定値をコンパイル時に変更できる。
>config type num = int;
上記の型の別名num
の値は、デフォルトではint
だが、コンパイル時にset
オプションにより変更できる。
逐次処理
式と代入
式文とは、式の直後にセミコロンを付記した文である。
式が評価され、その結果は受け取ることができない。
代入文は、左辺値の直後に代入演算子(= += -= *= /= %= **= &= \|= ^= &&= \|\|= <<= >>=
)と式を並べ、セミコロンを付記した文である。
式を評価した結果を左辺値に代入する。 左辺値は変数かフィールドである。
ブロック
複文とは、複数の文の並びをまとめ、その前後を括弧で閉じた文である。 内部の文は先頭から順に実行する。 複文は、レキシカルスコープを構成し、内部で宣言された変数や関数に対し、外部からの参照は禁止される。
>{
var a = 12;
writeln(a);
}
writeln(a);
条件分岐
条件分岐とは、式を評価した結果により、実行する処理が切り替わる構文である。
if
文とselect
文がある。 if
文の条件式はbool
型である。
else
節は不要な場合は省略可能である。 条件式を囲む括弧は不要である。
>if age < 20 then writeln("Adult Only") else writeln("Welcome");
予約語then
は条件式の範囲を明示する意味もある。
then
節に複文を記述する場合は、then
を省略できる。
>if language == "Kotlin" {writeln("I love Kotlin");}
select
文は多分岐を行う。条件式と等しいラベルを持つwhen
文を実行する。等値性は演算子==
で検証する。
>select animal {
when "cat" do writeln("meow");
when "dog" do writeln("bowwow");
otherwise writeln("gobblegobble");
}
当該のラベルがない場合、otherwise
文を実行する。
when
文とotherwise
文は、固有のスコープを有する。
条件反復
条件反復とは、条件式がtrue
である限り、処理を反復する構文である。
while do
文とdo while
文がある。
while do
文では、まず条件式が評価され、true
ならばdo
節を実行する。
do
節が終了すると条件式に戻る。
>while i < 1024 do i *= 2;
do while
文では、まずdo
節が実行され、条件式がtrue
ならばdo
節に戻り、false
ならば反復を終了する。
>do i *= 2 while i < 1024;
while do
文では、do
節が複文の場合、do
を省略できる。
なお、両文とも、条件式を囲む括弧は不要である。
要素反復
要素反復とは、イテレータが反復する限り、返値に対する処理を実行する構文である。 第 7.8節で解説する。
>for i in 1..100 do writeln(i);
大域脱出
break
文は、条件反復や要素反復を強制的に終了する。
continue
文は、条件反復や要素反復の先頭に戻る。
条件反復や要素反復が入れ子の場合、break
文やcontinue
文の対象は、最内の条件反復や要素反復である。
>for i in 1..10 do break;
ラベル文は、条件反復や要素反復に名前を与え、break
文やcontinue
文による大域脱出の対象に設定する。
>label foo for i in 1..10 do for j in 1..10 {
if i < j then continue foo;
writeln(i, ".", j);
}
値の交換
スワップ演算子 <=>
は、演算子の両辺の左辺値を交換する。
左辺値とは、代入文の左辺に指定する値である。
>var a = "golden axe", b = "silver axe";
a <=> b;
上記のスワップ演算子を始め、代入演算子は、返値をvoid
型と定義しており、実質的に文として機能する。
関数
第 7章では、Chapelのprocedureの仕様に言及する。
単にfunctionと呼ぶと、第 7.8節のiteratorも含む。
本稿では特記ない限り、関数はprocedureを指す。
下記は、int
型の引数を取るvoid
型の関数foo
である。
>proc foo(x: int, y: int): void {
writeln(x + y);
}
引数の型は引数の名前の後にコロンを挟んで記述する。 返値の型は引数宣言の後にコロンを挟んで記述する。 引数が複数ある場合はカンマ区切りで引数宣言を並べるが、引数がない場合は括弧そのものを省略して良い。
>proc hoge: void {
writeln("foo");
}
関数呼び出しは、関数名の後に実引数を記述する。 関数定義で括弧を省略した場合は、括弧を記述できない。
>foo(1, 2);
hoge;
実引数は、対応する仮引数の名前を指定して渡すこともできる。 例えば、下記の2行は同じ効果をもたらす。
>foo(x = 1, y = 2);
foo(y = 2, x = 1);
仮引数にはデフォルト値を指定できる。 デフォルト値を持つ仮引数は、関数呼び出しで実引数を省略できる。
>proc foo(x: int = 1, y: int = 2): void {
writeln(x + y);
}
可変長引数の関数は、実引数の個数が可変である。 可変長引数の実体はタプルである。 第 10章で解説する。
>proc foo(x: int ...): void {
for param i in 1..x.size do writeln(x(i));
}
可変長引数の関数は、実引数をカンマ区切りで並べることもできるし、代わりにタプルを渡すこともできる。
>foo(1, 2, 3);
foo((1, 2, 3, 4, 5));
関数を終了するにはreturn
文を実行する。 この時、返値を指定する。
返値の型は返値に合わせて指定する。
>proc foo(x: int, y: int): int {
return x + y;
}
void
型の場合は、返値なしでreturn
文を記述する。
内容がreturn
文のみの関数は、波括弧を省略できる。
>proc foo(x: int, y: int): int return x + y;
ラムダ式
ラムダ式は、無名関数を定義する式である。
下記は、引数x
とy
を加算する無名関数を定数add
に代入する。
>const add = lambda(x: int, y: int) return x + y;;
無名関数に限らず、関数は全てfirst class functionである。
int
型の引数を加算する関数plus
を定義する。
>proc plus(x: int, y: int): int return x + y;
first class functionは、変数に代入できる。
例えば、下記のプログラムは、plus
関数を定数add
に代入する。
>const add: func(int, int, int) = plus;
変数に代入した関数は、実引数を与えれば呼び出せる。 同様に、関数の引数に高階関数を渡すこともできる。
>proc apply(op: func(int, int, int), x: int, y: int): int return op(x, y);
なお、組込み関数func
は、関数型を表現する。
可変長引数であり、引数の型と返値の型を順番に指定する。
関数の修飾子
修飾子の前置により、リンカの挙動を制御できる。export
修飾子は、関数をプログラムの外部に公開する。
>export proc foo(x: int): int return 2 * x;
inline
修飾子は、関数を強制的にインライン展開して、関数を呼び出す全ての式を、関数の内容で置換する。
>inline proc foo(x: int): int return 2 * x;
外部で実装された関数を利用する場合は、extern
修飾子を使う。
下記はsched_getcpu
関数をリンクする。
>extern proc sched_getcpu(): c_int;
外部で実装された関数を利用する際は、プロトタイプ宣言を記述したヘッダファイルを指定する必要がある。
>int sched_getcpu();
$ chpl -o link link.chpl link.h
引数の修飾子
修飾子の前置により、関数の引数の挙動を制御できる。
param
修飾子は、引数を第 5.2節の静的定数にする。
type
修飾子は、引数を第 5.4節の型の別名にする。
下記のmakeTuple
関数は、指定の要件のタプルを返す。
>proc makeTuple(param dim: int, type eltType): dim * eltType {
const tuple: dim * eltType;
return tuple;
}
in
修飾子を前置した引数は、仮引数に実引数の複製が渡される。
引数への代入は、実引数に影響を持たない。
>proc foo(in x: int): void {
assert(x == 1);
x = 12;
}
var a = 1;
foo(a);
assert(a == 1);
out
修飾子を前置した引数は、仮引数に実引数の複製が渡されない。
引数への代入は、実引数に書き戻される。
>proc foo(out x: int): void {
assert(x == 0);
x = 12;
}
var a = 1;
foo(a);
assert(a == 12);
inout
修飾子は、in
とout
の双方の性質を持つ。
ref
修飾子を前置した引数は、passed by referenceになる。
返値の修飾子
修飾子の後置により、関数の返値の挙動を制御できる。
param
修飾子は、返値を第 5.2節の静的定数にする。
type
修飾子は、返値を第 5.4節の型の別名にする。
下記のmakeType
関数は、指定の要件のタプル型を返す。
>proc makeType(param dim: int) type return dim * int;
ref
修飾子は、返値を左辺値にする。
値を単に取り出すことも可能だが、代入式の左辺にすることもできる。
>var tuple = (0, 0, 0, 0, 0, 0, 0, 0, 0);
proc A(i: int) ref: int return tuple(i);
writeln(tuple);
for i in 1..9 do A(i) = i;
writeln(tuple);
上記のプログラムを実行すると、下記の出力を得る。
関数A
の返値は、まるで変数であるかのように見える。
(0, 0, 0, 0, 0, 0, 0, 0, 0)
(1, 2, 3, 4, 5, 6, 7, 8, 9)
関数の条件式
関数呼び出しは、実質的にテンプレート関数のインスタンス化である。
その条件はwhere
節で指定できるが、値が静的に確定する式に限る。
where
節は第 5.2節の静的定数と並び、generic programmingの根幹をなす。
下記のwhichType
関数は、引数x
の型を定数t
に格納し、条件式で検査する。
関数の呼び分けが実現する。
>proc whichType(x: ?t): string where t == real return "x is real";
proc whichType(x: ?t): string where t == imag return "x is imag";
静的な式はコンパイル時に計算される。 これを部分評価と呼び、例えば、階乗をコンパイル時に計算できる。
>proc fact(param n) param where n >= 1 return n * fact(n-1);
proc fact(param n) param where n == 0 return 1;
param fac6 = fact(6);
上記は、メタ関数を定義することにより、言語の機能を後天的に拡張するメタプログラミングの例でもある。 C でも、下記のテンプレートを定義すれば階乗をコンパイル時に計算できるが、直感的でなく煩雑である。
>template<int n> struct fact {
enum {val = n * fact<n-1>::val};
};
template<> struct fact<0> {
enum {val = 1};
};
constexpr int fac6 = fact<6>::val;
コンパイルエラーを出力するcompilerError
関数など、ある種の関数は、メタプログラミングに有用である。
関数の型推論
型推論とは、静的型付け言語で、変数や関数の引数、返値の型を省略しても、自動的に補完する機能である。
>proc foo(x, y) return x + y;
引数x
と引数y
の型は、関数のインスタンス化の際に、実引数の型から推論される。
返値の型も同様である。
>foo(1.0, 2.0);
引数の型を省略し、その型を関数の中で取得するには、クエリ式を利用する。
下記のxtype
がその例である。
>proc foo(x: ?xtype) {
select xtype {
when real do writeln(x, " is real");
when imag do writeln(x, " is imag");
}
}
ただし、省略された引数の型を取得するには、クエリ式を利用せずに、予約語type
を利用する方法もある。
>proc foo(x) {
select x.type {
when real do writeln(x, " is real");
when imag do writeln(x, " is imag");
}
}
クエリ式は、定義域が不明な配列を受け取り、定義域を取得する際にも利用する。 配列は第 13章に述べる。
>proc foo(x: [?D] ?t) {
writeln("domain is ", D);
writeln(typeToString(t));
}
入れ子関数
関数の定義は、他の関数の内部に記述できる。 これを関数のネスティングと呼び、関数の秘匿に利用できる。
>proc factorial(num: int): int {
proc tc(n, accum: int): int {
if n == 0 then return accum;
return tc(n - 1, n * accum);
}
return tc(num, 1);
}
イテレータ
予約語proc
の代わりにiter
を前置して定義した関数はcoroutineとなり、処理の中断と復帰が可能になる。
例えば、下記の関数foo
を何度も呼び出すと、最初は整数1
、次に整数2
、最後に整数3
を返して終了する。
>iter foo(): int {
yield 1;
yield 2;
yield 3;
}
coroutineは、yield
文を実行する度に処理を中断し、指定された返値を返す。return
文の記述は禁止する。
coroutineの実行は、前に実行したyield
文の直後で再開する。
典型的には、coroutineはfor
文で利用する。
>for i in foo() do writeln(i);
for
文は、指定されたcoroutineを何度も呼び出し、返値をインデックス変数に格納して、処理を実行する。
なお、連番を返すだけのcoroutineは、レンジ演算子で事足りる。下記は、1
から5
までの整数を出力する。
>for i in 1..5 do writeln(i);
自身を再帰的に呼ぶ関数を再帰関数と呼ぶ。 第 7章のprocedureだけでなく、coroutineも再帰関数にできる。
>iter foo(n: int): int {
if n > 0 then for i in foo(n-1) do yield i;
yield n;
}
イテレータ型とは、coroutineとしてthese
メソッドを実装したクラス型もしくはレコード型の総称である。
>class Foo {
var min: int;
var max: int;
}
クラスとレコードの詳細は、第 9章で解説する。
上記のクラスFoo
に、下記のメソッドthese
を実装する。
>iter Foo.these(): int {
for i in min..max do yield i;
}
these
メソッドの定義により、Foo
クラスはイテレータとしての機能を獲得し、for
文で利用可能になった。
>for i in new Foo() do writeln(i);
for
文は、イテレータのthese
メソッドを暗黙的に実行するが、明示的にcoroutineを指定しても構わない。
並列処理
タスク並列は粗粒度の並列化に適し、データ並列は細粒度の並列化に適す。 両者の詳細は、別著に述べる。
タスク並列処理
begin
文は、指定された文を実行するタスクを分岐させ、sync
文は、指定されたタスクの終了を待機する。
>sync begin writeln("hello, task!");
cobegin
文は、複文である。
波括弧の内部の各文をタスク分岐により並列処理して、全文の終了を待機する。
>cobegin {
writeln("1st parallel task");
writeln("2nd parallel task");
writeln("3rd parallel task");
}
writeln("task 1, 2, 3 are finished");
上記のcobegin
文は、begin
文によるタスク分岐と、sync
文による待機で、下記に示すように代用できる。
>sync {
begin writeln("1st parallel task");
begin writeln("2nd parallel task");
begin writeln("3rd parallel task");
}
writeln("task 1, 2, 3 are finished");
通常は、cobegin
文で事足りるが、タスクを再帰的に合流させる動作が不要な場合に、sync
文を利用する。
>inline proc string.shambles(): void {
proc traverse(a: int, b: int) {
if b > a {
const mid = (a + b) / 2;
begin traverse(a, 0 + mid);
begin traverse(1 + mid, b);
} else writeln(substring(a));
}
sync traverse(1, this.length);
}
なお、cobegin
文の内部に代入文を記述する場合は、with
節を記述して、代入する変数を示す必要がある。
>cobegin with(ref a, ref b) {
a = sched_getcpu();
b = sched_getcpu();
}
coforall
文は、並列化されたfor
文である。
反復処理を実行する多数のタスクを生成し、終了を待機する。
>coforall i in 1..100 do writeln("my number is ", i);
coforall
文はbegin
文とsync
文の糖衣構文である。
上記のプログラムは下記のプログラムと等価である。
>sync for i in 1..100 do begin writeln("my number is ", i);
データ並列処理
forall
文は、coforall
文と異なり、並列化の挙動を詳細に制御できるfor
文である。
第 8.4節で解説する。
>forall i in 1..100 do writeln("my number is ", i);
reduce
演算子は、多数の値を集計して少数の値に変換する演算子である。
これをリダクション演算と呼ぶ。
reduce
演算子の前に演算子(+ * && \|\| & \| ^ min max minloc maxloc
)を、後にイテレータを併記する。
>const sum = + reduce (1..1000);
上記のプログラムは、演算の競合を回避しつつ並列処理される点を除けば、下記のプログラムと等価である。
>for i in 1..1000 do sum += i;
演算子minloc
は最小値の位置を、maxloc
は最大値の位置を求める演算子だが、引数と返値がタプルである。
>var (value, idx) = minloc reduce zip(A, A.domain);
なお、zip
は引数に与えたイテレータを結合する。
例えば、zip(1..2, 3..4)
は(1, 3), (2, 4)
を返す。
scan
演算子は、リダクション演算の途中結果を順番に返すイテレータを返す。
これをスキャン演算と呼ぶ。
>for sum in + scan (1..100) do writeln(sum);
scan
演算子は、reduce
演算子と同様に、効率的に並列処理される。
時系列データの積分などに応用できる。
並列処理の禁止
serial
文は、条件式の値がtrue
の場合は、do
節内でタスクの分岐を禁止して、逐次処理を行う文である。
>serial true do begin writeln("executed");
並列イテレータ
forall
文とcoforall
文は、ともに並列化されたfor
文である点では同等と言えるが、その挙動は異なる。
>coforall i in 1..100 do writeln(i);
coforall
文は、begin
文の糖衣構文であり、イテレーション毎にタスクを分岐し、処理の終了を待機する。
>sync for i in 1..100 do begin writeln(i);
forall
文は、それ自体は並列化の機能を持たず、並列化に関する全ての判断をイテレータの裁量に委ねる。
>forall i in 1..100 do writeln(i);
forall
文は、以下に例示するleader
iteratorにより並列化し、末端の逐次処理はfollower iteratorで行う。
>iter range.these(param tag): range(int) where tag == iterKind.leader {
if this.size > 16 {
const mid = (this.high + this.low) / 2;
const (rng1, rng2) = (this(..mid), this(mid+1..));
cobegin {
for i in rng1.these(iterKind.leader) do yield i;
for i in rng2.these(iterKind.leader) do yield i;
}
} else yield this;
}
上記のleader iteratorは、レンジを再帰的に等分割しつつ並列化する。 下記のfollower iteratorも実装する。
>iter range.these(param tag, followThis) where tag == iterKind.follower {
for i in followThis do yield i;
}
以上のオーバーロードにより、forall
文で利用可能になる。
実質的に、forall
文は下記の糖衣構文である。
>for subrange in (1..100).these(iterKind.leader) {
for i in (1..100).these(iterKind.follower, subrange) do writeln(i);
}
複合型
複合型とは、第 3.2節の単純型の対義語であり、複数の単純型や複合型の組み合わせで定義される型である。
第 9章で取り上げるクラスやレコードは、複合型である。
他にも、タプルやレンジの実体はレコードである。
複合型の中でも、クラスは参照型の性格を有する。
new
演算子でインスタンス化し、delete
文で破棄する。
参照型とは、変数や即値がインスタンスを参照する型である。
明示的にインスタンス化を行う必要性がある。
>class Foo {}
const foo: Foo = new Foo();
delete foo; // OK
複合型の中でも、レコードは値型の性格を有する。
new
演算子でインスタンス化し、delete
文は禁止する。
値型とは、変数や即値がインスタンスとなる型である。
本来、明示的なインスタンス化や破棄は不要である。
>record Bar {}
const bar: Bar = new Bar();
delete bar; // NG
参照型の変数は、何らかの値を代入する以前は、有効なインスタンスを参照しない。
この状態をnil
と呼ぶ。
>class Foo {}
var foo: Foo = nil;
writeln(foo);
メンバ変数
フィールドとは、クラスやレコードがインスタンス毎に有する変数であり、複合型という名称の所以である。 フィールドの型に制限はなく、再帰的なクラスの定義も実現する。 下記は、リスト構造を実装する例である。
>class List {
var value: int;
var next: List;
}
特定のインスタンスのフィールドを参照するには、ドット演算子で、インスタンスとフィールドを指定する。
>var cons = new List();
var cdr = cons.next;
cons.value = 114514;
Chapelでは、フィールドへの参照は、左辺値を返すメソッドを暗黙的に呼び出す。 下記はその実体である。
>proc List.value ref: int return value;
フィールドと同名で、引数を持たず、左辺値を返すメソッドをアクセサと呼び、明示的な定義も可能である。
>proc Account.name ref: string {
if name != "Tomori Nao" then name = "Tomori Nao";
return name;
}
メンバ関数
メソッドとは、クラスやレコードに属する関数であり、必要であればインスタンスのフィールドを参照する。
>class Add {
const x, y: real;
proc print(): void {
writeln(x + y);
}
}
特定のクラスやレコードのメソッドを呼び出すには、ドット演算子で、インスタンスとメソッドを指定する。
>const add = new Add(x = 1, y = 2);
add.print();
定義済みのクラスやレコードにもメソッドを追加できる。 その場合は、クラスやレコードの名前を明記する。
>proc Add.add(): real return x + y;
生成と破棄
インスタンス化では、当該のクラスやレコードと同名のメソッドを実行する。 これをコンストラクタと呼ぶ。 コンストラクタは明示的に定義することもでき、その場合、引数は自由に設定できるが、返り値は禁止する。
>class Add {
const x: real = 0;
const y: real = 0;
proc Add(x: real, y: real) {
this.x = x;
this.y = y;
}
}
コンストラクタを省略した場合は、フィールドの初期値を引数に取るコンストラクタが自動的に定義される。
>var add2 = new Add(x = 1, y = 2);
この場合、引数にはデフォルト値が設定され、フィールドの変数宣言の初期化式の値がデフォルト値になる。
>var add1 = new Add(x = 1);
var add3 = new Add(y = 2);
クラスやレコードは、インスタンスが消滅する際に実行する処理を定義できる。 これをデストラクタと呼ぶ。 デストラクタは明示的に定義できる。 ただし、引数や返り値は禁止する。 下記は、デストラクタの例である。
>proc Foo.~Foo() {
writeln("deleted");
}
自身の参照
クラスやレコードが、インスタンスを明示的に参照する際は、予約語this
を用いる。
使用例を以下に示す。
>proc Account.setName(name: string): void {
this.name = "@" + name;
}
上記の代入文の左辺のname
はAccount
クラスのフィールドname
を、右辺のname
は引数name
を参照する。
参照型の性格を持つクラスに限らず、値型の性格を持つレコードでも、this
は参照である点に注意を要する。
>record Account {
var name: string;
}
上記のレコードAccount
のインスタンスを引数に、フィールドname
を変更する関数setName
を考えてみる。
>proc setName(account: Account, name: string): void {
account.name = name;
}
外部からは、フィールドname
は不変に見える。レコードは値型で、引数にはコピーが渡されるためである。
>var nao: Account;
setName(nao, "Tomori Nao");
writeln(nao);
予約語this
はコピーではなく参照であるため、意図する通りに自身のフィールドに代入することができる。
ファンクタ
this
メソッドを定義したクラスやレコードは、実質的に関数としても機能する。
これをファンクタと呼ぶ。
>class Add {
proc this(a: int, b: int): int return a + b;
}
上記のクラスAdd
は、2個の整数を引数に取り、加算を行うthis
メソッドを実装する。
定数add
に格納し、第 7.1節のfirst class
functionと同様に引数を与えて呼び出す。
123+45
を計算し、出力として168
を得る。
>const add = new Add();
writeln(add(123, 45));
継承と派生
継承とは、あるクラスやレコードが、他のクラスやレコードのフィールドやメソッドを引き継ぐことである。
>class Foo {
proc foo(): string return "foo!";
}
上記のクラスFoo
はfoo
メソッドを実装する。
これに対し、下記のクラスBar
もfoo
メソッドを実装する。
>class Bar: Foo {}
クラス名の直後にコロンを挟んでクラス名を記述すると、それを基底クラスとする派生クラスの宣言になる。
派生クラスBar
は基底クラスFoo
のフィールドやメソッドを有する。
必要に応じ、メソッドは再定義できる。
>proc Bar.foo(): string return "bar!";
上記は、Foo
クラスから継承したfoo
メソッドの挙動を再定義する例である。
これをオーバーライドと呼ぶ。
総称型
クラスやレコードのフィールドで、第 5.2節の静的定数や第 5.4節の型の別名を宣言すると、総称型になる。
>record Stack {
type eltType;
}
総称型は、型を引数に取る型であり、リスト等のデータ構造の実装を特定の型から分離する際に有用である。
>var a: Stack(eltType = uint);
var b: Stack(eltType = real);
上記の変数a
とb
は、両者ともにStack
型だが、相互に異なる型を持つ。
typeToString
関数で確認できる。
>writeln("a is ", typeToString(a.type));
writeln("b is ", typeToString(b.type));
上記のプログラムを実行して、下記の出力を得ることから、Stack
型は、型を引数に取る総称型だとわかる。
a is Stack(uint(64))
b is Stack(real(64))
総称型の引数となる型をparameterized typeと呼ぶ。 parameterized typeは型に限らず、静的定数でも良い。
構造的部分型
特定の関数を実装したクラスやレコードは、特定の仕様を満たす型と認定する。 これをduck typingと呼ぶ。
>class Duck {}
class Kamo {}
上記のクラスDuck
とKamo
は相互の継承関係にないが、同型の引数を宣言したquack
メソッドを実装する。
>proc Duck.quack(): string return "quack!";
proc Kamo.quack(): string return "quack!";
下記の関数duck
を定義する。
型が省略された引数x
を受け取り、引数x
に対しquack
メソッドを実行する。
>proc duck(x): string return x.quack();
下記のクラスIbis
を定義する。 前掲のクラスDuck
やKamo
を継承しない。
quack
メソッドも未実装である。
>class Ibis {}
Duck
とKamo
とIbis
のインスタンスをduck
関数に与える。
Ibis
クラスの場合はコンパイルエラーとなる。
>duck(new Duck()); // OK
duck(new Kamo()); // OK
duck(new Ibis()); // NG
関数が参照するメソッドやフィールドに基づき、引数の型を暗黙的に制限する仕組みを構造的部分型と呼ぶ。
タプル
タプルは、値をカンマ区切りで並べたデータ型である。
要素の型を揃える必要はなく、配列より軽量である。
括弧で囲むことで生成する。
個別の要素を参照するには、1から始まる番号を添えてthis
メソッドを呼ぶ。
>var names = ("Tom", "Ken", "Bob");
const ken = boys(2);
names(3) = "Robert";
タプル型は、要素の型をカンマ区切りで並べた組で表現する。 もしくは、要素の個数と型の乗算で表現する。
>const tup1: (int, int, int) = (1, 2, 3);
const tup2: 3 * int = (1, 2, 3);
タプルは、要素の型が揃っている場合、these
メソッドを呼び出すことにより、イテレータとして機能する。
>for boy in ("Tom", "Ken", "Bob") do writeln(a);
複数の変数や定数を宣言する際は、変数名や定数名をカンマ区切りで並べたタプルによる宣言も可能である。
>var (a, b): (string, int);
var (c, d): 2 * int;
同様に、複数の変数に対する代入も、タプルを利用して纏めることができる。 これをアンパック代入と呼ぶ。
>(a, b) = ("John", 24);
タプルによる変数宣言は、引数宣言にも適用できる。
下記は、タプル(y, z)
を引数に取る関数を定義する。
>proc foo(x: int, (y, z): (int, int)): int return x * (y + z);
第 7章の可変長引数の実体はタプルであるが、反対に、タプルを展開して固定長引数に与えることもできる。
>proc mul(x: int, y: int): int return x * y;
const mul23 = mul((...(2, 3)));
上記2行目の...
はタプル展開演算子であり、本来は固定長引数である関数mul
に、実引数2
と3
を与える。
範囲
レンジは、整数の範囲を表現するデータ型である。 第 12章の領域に比べて軽量で、実体はレコードである。
>const rng = 1..100;
レンジ演算子 ..
で生成する。
最小値と最大値を省略することにより、半無限区間や無限区間も表現できる。
>const from1toInf: range(int) = 1..;
必要に応じて、要素の個数は #
演算子で、周期は
by
演算子で、基準点の調整は align
演算子で指定できる。
>writeln(10..30 by -7 align 13 # 3);
要素の個数はsize
メソッドで取得できる。
最小値と最大値はlow
メソッドとhigh
メソッドで取得できる。
>assert((1..100).size == 100);
レンジ型は、要素の型と有界性と不連続性を指定して表現する。
>range(type idxType = int, boundedType = BoundedRangeType.bounded, stridable = false)
boundedType
は有界性を表現し、デフォルトはbounded
だが、最小値と最大値を省略すれば、無限になる。
- | - |
---|---|
bounded |
最小と最大の両方が有限の有限区間 |
boundedLow |
最小が有限で最大が無限の半開区間 |
boundedHigh |
最小が無限で最大が有限の半開区間 |
boundedNone |
最小と最大の両方が無限の無限区間 |
:
stridable
は不連続性を表現し、デフォルトはfalse
だが、by
演算子を使用した場合に限りtrue
になる。
>assert((1..3 by 2).stridable);
レンジは、these
メソッドを呼び出すことで、イテレータとして機能する。
典型的には、for
文で利用する。
>for i in 1..5 do writeln(i);
these
メソッドは、第 8.4節のleader iteratorとfollower
iteratorをオーバーロードし、並列化に対応する。
領域
領域は、空間や集合を表現するデータ型である。 第 11章のレンジに比べて高級で、実体はレコードである。 矩形領域は、レンジをカンマ区切りで並べて生成し、任意次元の矩形空間や、矩形配列の定義域を表現する。 連想領域は、包含する具体的な要素をカンマ区切りで並べて生成し、集合や、連想配列の定義域を表現する。
>const rectangular = {0..10, -20..20};
const associative = {"foo", "bar"};
矩形領域の場合、dims
メソッドでレンジのタプルに変換できる。
個別のレンジはdim
メソッドで取得する。
>var dom: domain(2) = {1..10, 1..20};
writeln(dom.dims());
writeln(dom.dim(2));
矩形領域の範囲を設定するには、レンジのタプルを引数に、setIndices
メソッドを実行する。
>var dom: domain(2) = {1..10, 1..20};
dom.setIndices((-100..100, 1..200));
矩形領域型は、次元数と要素の型と不連続性を指定して表現する。
>domain(rank: int, type idxType = int, stridable = false)
rank
は次元数で、rank
メソッドで静的定数として取得できる。
idxType
は、各次元の要素の型を表現する。
>param rank = {1..10, 1..20, 1..30}.rank;
stridable
は不連続性を表し、デフォルトはfalse
である。
連想領域型は、要素の型を指定して表現する。
>domain(idxType = string)
矩形領域と連想領域は、暗黙的にthese
メソッドを呼び出すことで、両者ともにイテレータとして機能する。
>for xyz in {1..10, 1..10, 1..10} do writeln(xyz);
for boy in {"Tom", "Ken", "Bob"} do writeln(boy);
these
メソッドは、第 8.4節のleader iteratorとfollower
iteratorをオーバーロードし、並列化に対応する。
配列
配列は、第 12章の領域を定義域に持ち、値域への写像を表現するデータ型であり、実体はレコードである。 配列の定義域により、矩形領域を定義域に持つ矩形配列と、連想領域を定義域に持つ連想配列に分類できる。
>const rectangular = [1, 2, 3, 4, 5, 6, 7, 8];
const associative = [1 => "one", 2 => "two"];
配列型は領域を括弧で括り、要素の型を指定して表現する。 領域を直に指定する場合、波括弧は省略できる。
>var A: [{1..10, 1..10}] real;
var B: [{'foo', 'bar'}] real;
配列の要素を参照する際は、要素のインデックスを引数として、左辺値メソッドthis
を暗黙的に実行する。
>A[1, 2] = 1.2;
A(3, 4) = 3.4;
上記のインデックスはタプルでも指定できる。 インデックスに領域を指定した場合、部分配列を取得できる。
>for (i, j) in A.domain do A(i, j) = i * 0.1 * j;
writeln(A(1..2, 1..3));
上記のプログラムを実行すると、下記の出力を得ることから、正しく部分配列を取得していることがわかる。
1.1 1.2 1.3
2.1 2.2 2.3
配列は、these
メソッドを実装し、イテレータとして機能する。
これは左辺値を返し、for
文で代入できる。
>var boys = ['Tom', 'Ken', 'Bob'];
for boy in boys do boy += '-san';
writeln(boys);
矩形配列で、値が0の要素が多く、全要素を格納するコストが過大である場合、sparse arrayも利用できる。
>var SpsD: sparse subdomain({1..16, 1..64});
var SpsA: [SpsD] real;
SpsD += (8, 10);
SpsD += (3, 64);
SpsA[8, 10] = 114.514;
配列の外見はレコードだが、内部的には、矩形配列や連想配列を実装するクラスのインスタンスを参照する。 領域も同様で、配列や領域を引数に取る関数はpass by valueだが、実質的にはpass by referenceに見える。
>proc foo(arr: [] int) {
arr = [2, 3, 4];
}
var A = [1, 2, 3];
foo(A);
writeln(A);
配列の代入は、右辺の配列の全要素を、左辺の配列の対応する要素に代入する操作である点に注意を要する。
>var A: [1..10] int;
var B = A;
for a in A do a = 1;
writeln("A is ", A);
writeln("B is ", B);
上記のプログラムを実行すると、下記の出力を得る。 これは、配列が参照型ではなく値型である証左である。
A is 1 1 1 1 1 1 1 1 1 1
B is 0 0 0 0 0 0 0 0 0 0
配列のエイリアスが必要な場合は、エイリアス演算子 =>
を利用する。
部分配列のエイリアスも作成できる。
>var A: [1..10] int;
var B => A[2..9];
for b in B do b = 1;
writeln("A is ", A);
上記のプログラムを実行すると、下記の出力を得る。
配列B
に対する操作が配列A
に反映することがわかる。
A is 0 1 1 1 1 1 1 1 1 0
配列は、区分化大域アドレス空間に従い、localeと呼ばれる分散ノードに適切に分散配置する機能を有する。
dmapped
演算子により、下記の矩形配列A
は格子状に分散配置され、矩形配列A
は周期的に分散配置される。
>use BlockDist, CyclicDist;
var A: [{1..10,1..10} dmapped Block(boundingBox={1..10,1..10})] real;
var B: [{1..10,1..10} dmapped Cyclic(startIdx=(1,1))] real;
分配された配列や領域に対し、localeが自身の領域を取得するには、localSubdomain
メソッドを利用する。
>for locale in Locales do on locale {
for (i, j) in A.localSubdomain() {
writeln((i, j), " @ ", here.id);
}
}
Locales
は実行環境で利用可能なlocaleを格納する配列であり、on
文は指定されたlocaleで文を実行する。
その他の型
列挙型
列挙型は、何らかのカテゴリを示す識別子の有限集合を表現するデータ型で、個別の識別子を列挙子と呼ぶ。
>enum Rabbit {Lapin, Lievre};
共用体
共用体は、状況によりデータ型が変化する変数に対し、適切な型を指定して参照可能としたデータ型である。
下記の共用体Pure
の場合、フィールドr
とi
は同じメモリ領域に占位する。
バイト境界は適切に調整する。
>union Pure {
var r: real;
var i: imag;
}
var pure: Pure;
pure.r = 19.19;
最後に値を代入したフィールドのみが有意である。 なお、クラスやレコードと同様にメソッドを定義できる。
同期型
下記は、sync
型とsingle
型の同期変数の利用例である。
タスクは、同期変数release$
の更新を待機する。
>var count$: sync int = 32;
var release$: single bool;
coforall i in 1..32 {
const mc = count$;
if mc != 1 {
count$ = mc - 1;
writeln("wait");
release$;
} else release$ = true;
}
同期変数は、変数値の他にfullかemptyの状態を有する。
初期値はemptyで、代入によりfullに遷移する。
同期変数への代入はempty時のみ、参照はfull時のみ可能である。
さもなければ、遷移するまで待機する。
sync
変数のみ、変数値の参照でemptyに遷移する。
反対に、代入済みのsingle
変数は再代入を拒絶する。
モジュール
名前空間とは、変数や関数が可視となる範囲をmodule
文で区分し、衝突の可能性を回避する仕組みである。
>module Foo {
const piyo = "piyo";
proc hoge(): string return "hoge";
}
上記の名前空間Foo
で宣言された変数piyo
や関数hoge
は、外部からの参照にドット演算子が必要になる。
>writeln(Foo.hoge());
use
宣言を記述すれば、記述した名前空間の内部では、対象の名前空間に対し、ドット演算子を省略できる。
>use Foo;
writeln(piyo);
明示的な名前空間に属さない文は、ファイル名から拡張子.chpl
を除去した名前の名前空間に暗黙的に属す。
名前空間は、プログラムの起点となるmain
関数を明示的に定義できる。
引数はなく、返値はint
型である。
>proc main() {
writeln("Hello, World!");
}
プログラムが起動すると、main
関数を定義した名前空間が初期化され、名前空間の直下の文が実行される。
依存関係に従い、全ての名前空間が初期化すると、main
関数を実行する。
main
関数は重複して定義できる。
>module Foo {
proc main() {
writeln("This is Foo");
}
}
module Bar {
proc main() {
writeln("This is Bar");
}
}
この場合、どの名前空間に属すmain
関数でプログラムを起動するか、コンパイル時に指定する必要がある。
$ chpl --main-module Foo module.chpl