※本記事は、Jason Tattonによる”Concurrent programming with Concurnas“を翻訳したものです。
現在の開発者が利用できる今までにないハードウェアの力。新しいJVMプログラミング言語Concurnasでその力を解放する方法に迫る
著者:Jason Tatton
2020年6月22日
並列プログラムを書くのは難しい作業です。しかし、現在の最新マルチコアCPUアーキテクチャの力を利用するためには、同時実行に対応したソフトウェアが欠かせません。さらに、現在のほとんどのコンピュータはグラフィックス・プロセッシング・ユニット(GPU)を利用できます。開発者がGPUを利用すれば、大量の並列問題をこれまでにないスピードで解くこともできます。しかし、GPU向けのネイティブ・コーディングも困難です。
開発者がソフトウェアを構築する方法は変わりました。本質的にイベントドリブンやリアクティブなシステムが増え続けている一方で、ほとんどのプログラミング言語やアルゴリズムはこの新しいパラダイムをうまくモデリングできていません。この課題の発生と時を同じくして、開発者は生産性を向上させて、信頼性、パフォーマンス、およびスケーラビリティに優れたソフトウェアを作成することが期待されています。イノベーションあふれるソリューションでそれらの問題を解決するときが来ているのです。
本記事では、JVMをターゲットとした新しいプログラミング言語Concurnasについて説明します。Concurnasは、信頼性、スケーラビリティ、高パフォーマンス、同時実行性を兼ね備えた最先端の分散型パラレル・システムの構築用に特化して設計されました。
Concurnas言語の概要
Concurnasは、主に次の4つのことを目指して設計されました。
- 平均的な開発者が、現在のマルチコアCPUおよびGPUハードウェア・アーキテクチャを活用した同時実行パラレル・システムの構築を容易にして扱いやすくする
- 強力な静的型システムがもたらすパフォーマンスと機能を備えた、動的型付け言語の構文を提供し、学習しやすく、高パフォーマンスの言語となる
- 経験を積んだソフトウェア・エンジニアでも、コンピューティング以外の分野のスペシャリストでも役立つものとなる
- 命令型、オブジェクト指向、関数型、リアクティブ・プログラミングの各パラダイムから特に人気の高い機能を簡潔な言語で提供し、プロトタイピングと大規模ソフトウェアの両方をサポートするマルチパラダイム言語サポートを提供する
Concurnasでは、JVMで実行するためのコンパイルが行われます。そのため、JVMの優れたパフォーマンス、自動メモリ管理(すなわちガベージ・コレクション)、Just-In-Timeコンパイル、Javaの標準ライブラリへのアクセス(以降の例で説明します)、ツールの使用に加え、JavaやScala、Kotlinといった既存JVM言語との互換性を実現します。
Concurnas言語のコア構文は、Pythonから着想を得ています。次の単純な関数では、与えられた数値と25との最大公約数を計算しています。
def gcd(x int){
y = 25
while(y){
(x, y) = (y, x mod y)
}
x
}
このコードに関して注目すべき点は、以下のとおりです。
- 変数yの型はinteger型と推論されます。関数全体の戻りタイプも同様です。Concurnasでは、使いやすさのため、関数の入力パラメータを除いて型付けはオプションです。型推論(関数型プログラミング言語によく見られる機能)が使用されており、動的型付け言語のように見えます。
- 関数の定義では、return文は省略されます。すべてのコード・ブロック({}で囲われている部分)は値を返すことができます。最後に参照された値(この例ではx)が暗黙的に返されます。
- Concurnasではタプルをサポートしています。(y, x mod y)というタプルの定義があることがわかります。代入演算子の左辺の(x, y)には、分割代入が行われます。タプルはすばらしい手段です。タプルを使うことにより、データのコレクションをまとめるラッパー・クラスを定義することなく、コレクションを扱うことができます。
- ブール式が来ることが想定される場所(while(y)の中など)で、すべての型を使うことができます。
- 関数は独立して存在できます。Javaなどの言語では、関数をクラスのコンテキスト外に定義することはできませんが、Concurnasにはそのようなルールはありません。
Concurnasの同時実行性
Concurnasでは、Javaやその他多くのJVM言語に見られる標準的な同時実行モデルに代わる革新的な仕組みが提供されます。Concurnasの同時実行モデルでは、スレッドやクリティカル・セクション、ロック、共有メモリを明示的に管理する必要はありません。
これは、アイソレートと呼ばれる、スレッドに似た軽量なコンテナで計算を実行することにより、実現しています。Concurnasプログラムのすべてのコードはアイソレートで実行されます。アイソレートは同時実行が可能ですが、メモリを直接共有することはできません。
異なるアイソレート間でデータが参照される場合、データはコピーされます。そのため、意図せずに状態が共有されることはありません。当然ですが、これが話の全容であれば、Concurnasはあまり機能的でないプログラミング言語となるでしょう。安全な形で状態を共有して管理することが、並列コンピューティングには欠かせないからです。
これを実現するため、Concurnasにはrefと呼ばれる特殊な型が存在します。refは任意の型から構成可能であり、型にコロン(:)を追加して示します。refではその同時実行性が安全かつアトミックに管理されるため、コピーすることなくアイソレート間でrefの共有が可能です。アイソレートからの戻り値が見込まれる場合、その戻り値はref型になります。次に示すのは、先ほど定義した関数gcdを使ってアイソレートを作成する方法です。
res1 int: = gcd(92312)!
res2 = {gcd(2438210)}!
larger = res1 if res1 > res2 else res2
larger = res1 if res1 > res2 else res2
ここでは、感嘆符演算子(!)を使って2つのアイソレートを作成しています。!演算子は、関数呼出しやコード・ブロックの後に配置できます。
この場合、いずれのアイソレートの戻りタイプもint:になります。res1は明示的にint:型として定義しましたが、res2については明示的な定義を省略しました。続いて、コードでは2つの計算を同時に行い、その結果をres1とres2に格納して、いずれが大きいかを判定しています。プログラムの最後の文に到達すると、コードの実行は一時停止し、res1とres2の両方が設定されるまで待機します。
実行が一時停止している間は、コードを実行していた、ベースとなるアイソレートが実行権を手放すため、他のアイソレートを実行できるようになります。res1とres2が設定されると、ベースとなるアイソレートに制御が戻されます。するとベースとなるアイソレートでは、関連する各refが参照する値が取得され、それぞれの値を使ってコードの後続処理が実行されます。
設定される1つまたは複数のrefの状態に応じてアイソレートの実行を一時停止するためには、awaitキーワードを使用します。このキーワードにはオプションでガード条件を付けることもできます。次の例をご覧ください。
result int:
{result = 100}!
await(result)
//実行の継続が可能に...
syncキーワードを使うこともできます。このキーワードを使うことにより、コード・ブロック内で作成されたすべてのアイソレートの実行が完了してから、実行が継続されることが保証されます。
result int:
sync{
{result = 100}!
//...more isolates defined here...
}
assert result == 100
このアイソレート・モデルが美しいのは、開発者がコードの大部分をシングルスレッドの要領で書くことができ、並列処理制御を行う必要がない点です。並列設定でそのコードを実行する場合でも、アイソレートの生成により、コードがシングルスレッド設定で実行されているかのように振る舞うことがやはり保証されます。このモデルにより、コード内の関心領域が分割されます。
この処理の大部分は、依存データをアイソレート間で暗黙的にコピーすることによって行われます。次に例を示します。
n = 10
nplusone = { n += 1; n }!
nminusone = { n -= 1; n }!
assert n == 10
assert nplusone == 11
assert nminusone == 9
上記のコードでは2つのアイソレートを生成しています。いずれも変数nを操作するものです。アイソレートの実行は本質的に不確定です。そのため、2つのアイソレートが厳密に同じ時間に実行されるようにスケジュールされる可能性もあれば、最初のアイソレートが2つ目のアイソレートの前に実行される可能性や、2つ目が先に実行される可能性もあります。この順番を知ることはできませんが、これは重要なことではありません。
ベースとなるそれぞれのアイソレートがどのように実行されても、依存データはコピーされるため、常に同じ結果が導き出されます。そして、元のnは変化しません。このようにデータが暗黙的にコピーされるということは、複雑な不変データ構造を導入する必要がないということでもあります。つまり、そういった不変データ構造ではなく、大学のコンピュータ・サイエンスの基礎で学んだおなじみの可変データ構造を使うことができます。現実世界のほとんどのソフトウェアでは、可変データ構造が使われています。
アプリケーションでデータ構造を扱わなければならない場合もあります。何メガバイトもあるような巨大なデータ構造を扱うこともあるでしょう。このようなデータ構造は、複数の同時実行エンティティで共有する必要があります。そのような状況では、データのコピーによってシステムのパフォーマンスに悪影響が生じるでしょう。そのためにConcurnasに導入されているのがsharedキーワードです。このキーワードの使用により、アイソレートが「共有状態は存在しない」という条件に違反することが可能になります。このキーワードは、読取り専用の巨大なデータ構造を使う場合や、スレッドセーフであることがわかっている(つまり、並列設定での実行に適している)他のJVM言語のクラスを参照する場合に、優れたオプションとなります。次に例を示します。
shared lotsOfNumbers = x for x in 0 to 100 step 3
//sharedを付けることで、アイソレート間でlotsOfNumbersがコピーされなくなる
def filteredDataSet(alist java.util.List<int>, modof int){
x for x in alist if x mod modof == 0
//上記は、リストを走査してリストを返す例
}
mod15 = filteredDataSet(lotsOfNumbers, 15)!
//mod15 ==> [0, 15, 30, 45, 60, 75, 90]
mod16 = filteredDataSet(lotsOfNumbers, 16)!
//mod16 ==> [0, 48, 96]
上記のような形でデータを共有する、別のアプローチに、アクターを使う方法があります。アクターは、コード内でマイクロサービスを生成する手段として優れています。アクターは、Javaなどのオブジェクト指向言語における通常のクラスによく似ており、アイソレートに似た単純な計算モデルをエクスポートします。このモデルでは、アイソレート内のすべてのコードがシングルスレッド的に実行されます。これは、アクター内で専用のアイソレートを暗黙的に生成することで行っています。このアイソレートの仕事は、アクターに対して行われた呼出しを実行することのみです。アクターを以下に示します。
actor IdGenerator(prefix String){
//IdGeneratorには、上記のStringを受け取る1つのコンストラクタがある
cnt = 0//暗黙的にプライベート状態
def getNextId(){
toReturn = prefix + "-" + cnt
cnt += 1
toReturn
}
}
//使ってみ
idGen = IdGenerator("IDX")//アクターを作成する
anId1 = idGen.getNextId()//==> IDX-0
anId2 = idGen.getNextId()//==> IDX-1
上記で、IdGeneratorのgetNextIdメソッドの呼出しはアクター内の操作であるため、複数のアイソレートがこのメソッドを呼び出しても、IdGenerator内の状態が矛盾する可能性はありません。そのため、複数のアイソレートに同じ値のIDが2回返されることはありません。これがJavaであれば、ロックやsynchronizedキーワードを使って、getNextIdを囲むクリティカル・セクションを明示的に作成する必要があるでしょう。
既存のクラスのアクターを作成することもできます。次に示すのは、HashSetからアクター・サービスを作成する方法です。
setService = actor java.util.HashSet<int>() setService.add(65)
作成できるアイソレートの数は、コードを実行する物理マシンで利用できるメモリの量によってのみ制限されます。この制限値は、JVMで生成できるスレッド数よりもはるかに大きくなっています。そのため、アイソレートによる同時実行モデルは従来型のアプローチよりもはるかにスケーラブルです。また、同時実行によるメリットをソリューションで生かしたいライブラリ作成者は、さらにスレッドを作成することや、必要なスレッドをホスト・プログラムからライブラリに注入することなく、Concurnasによってパラレル処理を実現できます。
Concurnasによるリアクティブ・コンピューティング
Concurnasのrefには、アイソレートが状態の変更に関する通知を受信できるという、他にはない特性があります。この方法により、開発者にリアクティブ・コンピューティングの機能がもたらされます。
金融アプリケーションを構築しているとします。このアプリケーションでは、ある資産の相対価格が別の資産の相対価格を上回ったときに、株式市場で1回の取引を開始するものとします。このアプリケーションでは、市場データのフィードを2つ受け取り、最新の状態に対して何らかの計算を行う必要があります。その計算結果に応じて、何らかのアクションを開始する可能性があります。この動作は、everyキーワードを使って実装することができます。
asset1price int:;
asset2price int:;
every(asset1price, asset2price){
if(asset1price > asset2price){
//...ここで取引アクションを開始する
return//後ほど呼び出されるeveryブロックを終了する
}
}
上記のeveryブロックは、asset1priceまたはasset2priceが更新されるたびに呼び出されます。リアクティブ・コンポーネント自体が、返された値のストリームを保持するrefを返すこともできます。そのため、リアクティブ・コンポーネントを連鎖させることも可能です。次の例をご覧ください。
a int:
b int:
c = every(a, b){
a + b
}
every(c){
System.out.println("latest sum: {c}")
}
上記のcは、aまたはbが変更されても、その都度aおよびbの合計と等しくなります。このコードには、もう1つのeveryブロックが含まれています。このブロックでcの最新の値を出力しています。
Concurnasにより、こういった種類のリアクティブ・システムを表現するための自然な方法が提供されます。しかし、Concurnasは書きやすく読みやすいコンパクトな言語となることも目指しています。そこで、上記の構文のさらにコンパクトな形を次に示します。
c <= a + b
この場合もcは、aまたはbが変更されても、その都度aおよびbの合計と等しくなります。
同時実行の内部処理
Concurnasの同時実行モデルは、継続渡し(continuation passing)を使って実装されています。これは、Project Loomで継続の実装に使われているアプローチととてもよく似ています。アイソレートは実行時に、ベースとなるハードウェアの一定数のスレッドにマッピングされます。この数は、ベースとなるハードウェアで実行に利用できる計算能力(利用できるCPUコアの数など)に基づいて生成されます。
アイソレートがコードの実行を一時停止する必要があるポイントに到達した場合(値がまだrefに割り当てられていないのに、そのrefの参照先を取得する場合など)、そのアイソレートを実行している、ベースとなるワーカー・スレッドのインスタンスに実行が到達するまで、プログラムの現在の実行スタックがコール・チェーンをパッケージングします。この時点で、別のアイソレートが利用できる場合、実行はそのアイソレートに移ります。そうでない場合、実行は、一時停止したアイソレートの実行を再開できるという通知(たとえば、実行を一時停止していたrefに設定された値)を受け取るまで待機します。
このアプローチには、わずかにパフォーマンスのペナルティがあります。実際のところ、アイソレート間で切り替えが頻発するアプリケーションでは、実行時間が5%ほど余分にかかることがわかっています。この値は、このような変換機能にしては十分小さく妥当だと思います。
現在、アイソレートのスケジューリングの仕組みは、ラウンド・ロビンがベースになっています。ただし、Concurnasの言語とランタイム・プラットフォームが進化するにつれて、Java Fork/Joinで公開されているワークスティーリング型スケジューラを活用することや、エンドユーザーが独自のスケジューラを定義できるようになることで、適切にリソースを分配するようになるかもしれません。このスケジューリングの仕組みは、ガベージ・コレクタの選択や微調整のようなもので、ほとんどの開発者にとっては心配する必要のないことです。当然ですが、この機能が非常に有用だと考える開発者もいます。
スタックを巻き戻す、アイソレート間で状態をコピーする、コードを同時実行に適した状態にする、というこのプロセスは、実行時にConcurnasのクラス・ローダーの中で行われます。これにより、他のJVM言語で書かれたコードとの互換性を実現しています。プラットフォームのパフォーマンスは徐々に改善されていきますが、今後のバージョンのConcurnasは、現在コンパイルされているConcurnasのコードとの互換性を備えたものになる予定です。
グラフィックス・プロセッシング・ユニット
ほとんどの最新コンピューティング・プラットフォームには、GPUが搭載されています。GPUは大量のデータをパラレルに計算するチップで、SIMD(単一命令複数データ)操作や一般的にCPUの制約を受ける問題を解くのに最適です。
CPUベースのアルゴリズム実装と比較して、GPUではSIMD問題を100倍以上高速に解ける場合があります。さらに、ギガフロップ当たりの消費電力とハードウェア費用も、CPUベースの計算よりかなり少なく済みます。その理由は、最新のCPUとGPUの仕様を見れば明らかです。
たとえば、最高クラスのグラフィックス・カードであるNVIDIA GeFORCE RTX 2080 Tiには、並列に計算を行えるコアが4,352個あります。一方で、最新のCPUのコアは64個でしょう。GPUコアのクロック数はCPUコアよりもかなり小さいですが、適切な計算タスクであれば、CPUの数桁倍に及ぶ、GPUのコンピューティング能力を利用できます。世界最先端のスーパーコンピュータの多くが、専用のGPUハードウェアを使ってパラレル化を行っています。
ただし、よいことばかりではありません。GPUのプログラミングは難しいこともあります。GPUは一部の(とはいっても、かなり範囲は広くなっています)種類のSIMD問題を解くことにのみ最適化されているばかりでなく、開発者にも、ベースとなるハードウェアを深く理解し、Cなどの低レベル言語でハードウェア・プログラミングを行うことが求められてきたのが通常です。
Concurnasでは、GPUプログラミングと、それに関連するパラレル計算構造をビルトインでサポートしています。Concurnasでは、Concurnasらしいコードを書くことにより、GPUを活用して効率的なパラレル計算を行うことができます。これにより、GPUコンピューティングを活用するハードルが大幅に下がります。コードは、GPUのカーネル内で実行されます。カーネルを並列に実行するのは、GPUの個々のコアです。
それでは、カーネルの簡単な例を見てみます。AおよびBという2つの配列の各要素について、y = a**2 + b + 10を計算します。
gpukernel 1 twoArrayOp(global in A float[], global in B float[], global out result float[]) {
idx = get_global_id(0)
result[idx] = A[idx]**2 + B[idx] + 10
}
このカーネルでは、GPUのグローバル・メモリ空間(CPUアーキテクチャのRAMに類似)に存在する3つの配列を操作しています。このコードでは、カーネルが操作する配列が入力配列または出力配列のいずれであるか(混合モードも可能)を指定しています。これにより、コンパイラで生成されるマシン・コードをGPU向けに最適化することができます。なお、カーネルでは、出力変数に結果を書き込むことはできるものの、値を返すことはできないという点に注意してください。get_global_id(0)という呼出しでは、呼び出すコアに対して、配列のどの要素を操作するかを表すコンテキストを提供しています。コードでは、GPUを特定し、そのGPUにデータをコピーする必要があります。
//GPUデバイスの選択... device = gpus.GPU().getGPUDevices()[0].devices[0] //このGPUにサイズが10である3つの配列を作成、2つは入力用 inGPU1 = device.makeOffHeapArrayIn(float[].class, 10) inGPU2 = device.makeOffHeapArrayIn(float[].class, 10) //1つは出力用 result = device.makeOffHeapArrayOut(float[].class, 10) //GPU上の配列に書き込む c1 := inGPU1.writeToBuffer([ 1.f 2 3 4 5 6 7 8 9 10]) c2 := inGPU2.writeToBuffer([ 1.f 2 1 2 3 1 2 1 2 1])
writeToBufferはコピー操作で、ref(c1とc2)を返します。この操作は非同期であるため、これが実行されている間も、コードでは他の操作を同時に実行できます。後続のGPU操作がこのrefを引数として受け取って、GPU上に操作のパイプラインを構築することができます。スループットが重要になる高パフォーマンス・コンピューティング環境では、GPUでのデータ処理と並行して、GPUとの(他の)データのやり取りを行うことが最善です。
Concurnasでは、GPUをコプロセッサとしてモデリングしています。そのため、GPUの操作は非同期式に行われ、システム内の他のアイソレートと並列に実行されます。
次に、twoArrayOpカーネルへのハンドルを作成します。
inst = twoArrayOp(inGPU1, inGPU2, result) compute := device.exe(inst, [10], c1, c2)//10コアでtwoArrayOpの処理を実行 ret = result.readFromBuffer(compute)
twoArrayOpカーネルに境界付きの入出力変数を渡すと、instハンドルが得られます。次に、このハンドルをデバイスに渡し、exeメソッドで実行しています。このコードでは、GPUの実行コア数と、特定のrefも指定しています。特定のrefとは、そのrefが完了するまで実行を待機しなければならないものです。今回の場合、該当するrefはc1およびc2です。この2つは、GPUにデータをコピーする操作に対応します。これは非同期操作で実行されるため、返されるcomputeもrefになります。最後に、computeが完了してから、結果バッファresultから計算結果を読み取っています。
ハードウェア・モデルによっては、GPUでグローバルおよびローカルの専用メモリ空間を利用できる場合があります。空間的局所性が高いアルゴリズム(行列の乗算など)を開発する場合、ローカル・メモリを使うことで、グローバル・メモリを使った場合よりもパフォーマンスが何桁も向上する場合があります。Concurnasでは、GPUの各種メモリ空間もサポートしています。
GPUから最高のパフォーマンスを引き出すためには、GPU計算の細部をある程度理解しておく必要があります。しかし、Concurnasを使うことで、このプロセスは大幅に簡略化されます。以上の機能はすべて、ConcurnasのGPUカーネル・コードをOpenCL Cにトランスパイルすることで実現されています(トランスパイルとは、ある言語で書かれたコードを受け取り、別の言語に変換することを指します)。OpenCLはGPU計算用のマルチプラットフォームAPIで、AMD、Intel、NVIDIAという三大GPUメーカーがサポートしています。トランスパイルしたコードは、アノテーションを使ってコンパイル済みのクラス・ファイルにアタッチし、実行時にGPUに渡します。この仕組みによって、移植性を実現しています。
マルチパラダイム・プログラミング
Concurnasはマルチパラダイム言語です。つまり、命令型、関数型、オブジェクト指向、リアクティブ・プログラミングの各パラダイムから、もっともよく使われている最適な側面だけを選ぶことができます。
オブジェクト指向プログラミングと言えば、Concurnasではクラスを定義するコンパクトな仕組みを提供しています。多くのデータ指向クラスを1行のコードで表現できます。次の例をご覧ください。
abstract class UniversityMember(~enrolled boolean)
class Person(~firstname String, ~surname String, ~yob short, enrolled = false) < UniversityMember(enrolled)
//インスタンス・オブジェクトを作成
p1 = new Person("dave", "brown", 1970)
p2 = Person("sandy", "smith", 1978, true)//newキーワードの使用はオプション
クラス名の後の括弧内にあるのはフィールド定義です。この定義では、フィールドを設定できるコンストラクタを自動生成するように、Concurnasに伝えています。enrolledフィールドにはデフォルト値false(さらに、型はBooleanであると推論されます)があり、Personクラスのインスタンスを作成する際にオプションでこのフィールドに値を指定できる点に注意してください。上記の例のPersonはUniversityMemberを継承しています。継承は、extendsまたは<キーワードを使うことで表します。Personは、入力パラメータの1つをUniversityMemberに渡すことで、UniversityMemberのスーパーコンストラクタを満たします。そしてこの入力パラメータは、Personクラスのフィールドとしては作成されません。
Concurnasでは、先頭にチルダ(~)文字が付いているすべてのフィールドについて、getterとsetterが自動生成されます。ちなみに、マイナス記号(-)の場合はgetterのみ、プラス記号(+)の場合はsetterのみが生成されます。次に例を示します。
p1 = new Person("dave", "brown", 1970)
name = p1.firstname //equivalent to: name = p1.getFirstname()と同じ
p1.firstname = "newName" //equivalent to: pe.setFirstname("newName")と同じ
Concurnasでは、多くのモダンJVM言語と同じように、等価演算子(==)はほとんどの開発者の想定どおりに動作します。つまり、参照の等価性ではなく値の等価性を評価します。次のコードは有効です。
p1 = Person("dave", "brown", 1970)
p2 = p1@ //p1のディープ・コピー
assert p1 == p2 //オブジェクトは異なるが値は同じ
assert not (p1 &== p2) //参照の等価性は&==
上記のコードでは、@コピー演算子を使っている点にも注意してください。この演算子では、左辺で参照しているオブジェクトのディープ・コピーを行います。
Concurnasでは、すべてのクラスについて、hashcodeメソッドとequalsメソッドが自動生成されます。こうすることによって、セットやマップなどの一般的なデータ構造でそれらのメソッドを簡単に使えるようにしてます。次の例をご覧ください。
p1 = new Person("dave", "brown", 1970)
myset = set()
myset.add(p1)
p2 = new Person("dave", "brown", 1970)
assert p2 in myset
前述のように、Concurnasではタプルをサポートしています。さらに、継承による排他的なクラス作成とは対照的に、合成によってクラスを作成できるトレイトもサポートしています。
Concurnasには、以前から関数型プログラミング言語に見られる多くの機能も含まれています。たとえば、メソッド参照や部分関数です。
//関数の参照 funcRef (int, int) int = plus&(int, int) //通常の関数のように呼び出す result = funcRef(2, 3) //=> 5 //部分関数 plusone (int) int = plus&(int, 1) //これも通常の関数のように呼び出す result = plusone(10) // ==> 11
さらに、パターン・マッチングがサポートされています。パターン・マッチングはJavaのswitch文に似ていますが、それよりもはるかに強力な仕組みです。
def matcher(an Object){
match(an){
Person(yob < 1970) => "Person.Born: {an.yob}" //Person型内部のガード・チェック・フィールド
Person => "A Person" //Person型かどうかをチェック
int; < 10 => "small number" //intかどうかをチェック、値が10より小さいことを求めるガード
int => "another number"
x => "unknown input" //その他すべての入力に一致
}
}
res = matcher(x) for x in [Person("dave", "brown", 1829), Person("dave", "brown", 2010), "oops", 43, 5]
//res ==> [Person.Born:1829, A Person, unknown input, another number, small number]
パターン条件は、最初から最後に向かう順番でチェックされます。型とのマッチングと、ガード(オプション)もサポートされています。ガードは、オブジェクトのフィールドに対して指定することもできます。
Concurnasで提供されている、関数型プログラミングのその他の側面には、先ほど取り上げた型推論や、ラムダ式、遅延評価などがあります。
データの操作
データ・サイエンスや数値計算のみならず、ほとんどのエンタープライズ・ソリューションでは、何らかの形でデータを操作します。これを実現するため、Concurnasでは一般的なデータ構造を操作する強力な方法を提供しています。それでは、いくつかのデータ構造を定義してみます。
anArray = [1 2 3 4 5 6]
aList = [1,2,3,4,5,6]
aMatrix = [1 2 3 ; 4 5 6]
aMap = {"one" -> 1, "two" -> 2, "three" -> 3}
サポートされている操作には、以下のようなものがあります。
cont = "one" in aMap //マップ内の値をチェック longNames = aMap[key] for key in aMap if key.length() > 3 //マップのキーを使った反復 arrayValue = anArray[2] //配列の個々の値 subarray = anArray[4 ...] //サブ配列、[5 6]
Concurnasでは、リストの走査をサポートしています。これは実質的にforループのシンタックス・シュガーで、次の例のi mod 2 == 0のようなガード文を追加できます。
ret = i+10 for i in aList if i mod 2 == 0
Concurnasでは、ベクトル化もサポートしています。ベクトル化とは、リストまたはn次元配列の各コンポーネントに対して同じ要素単位の操作を行う場合に適した技術です。たとえば、行列の各要素xに対してy = x**2 + 1を計算したい場合、次のようにすることができます。
mat = [1 2 ; 3 4]
mat2 = mat^*2 + 1 //==> [3 5 ; 7 9]
mat^^*2 + 1 //インプレース・ベクトル化操作
数値の範囲は、第一級オブジェクトとしてサポートされています。値は遅延作成されるため、無限の範囲を作ることも可能です。
numRange = 0 to 10 //[0, …, 10]の範囲
tepRange = 0 to 10 step 2 //[0, 2, …, 10]の範囲
revRange = tepRange reversed //上記を逆順にした範囲([10, 8, …, 0])
decRange = 10 to 0 step 2 //[10, 8, …, 0]の範囲
infRange = 0 to //[0,… ]の無限列
steInfRa = 0 to step 2 //[0, 2, … ](2ずつ増加する無限列)
decInfRa = 0 to step (-1) //[0, -1, … ](1ずつ減少する無限列)
val = x for x in numRange //範囲のリスト走査
check = 2 in numRange //値の存在をチェック
Concurnasのインストール
この言語を使う前に、Java JDK 1.8以降をインストールする必要があります。その後、Concurnasをインストールできます。
Concurnasのダウンロード・サイトでは、Windows用のインストーラと、他のプラットフォーム用のzipファイルが公開されています。なお、LinuxではSDKMAN!SDKマネージャで、次のコマンドを使用してConcurnasをインストールする必要があります。
$ sdk install concurnas
JDKとConcurnasをインストールした後は、hello.concという名前で簡単な「hello world」アプリケーションを作ってみます。
for(x in 1 to 5){
System.out.println("hello world {x}")!
}
Concurnasは、Javaなどの言語と同じく、コンパイルと実行という標準的な2段階のプロセスに従います。hello.concコード・ファイルを保存したら、conccコマンドでコードをコンパイルすることができます。次に例を示します。
$ concc hello.conc
コンパイルしたコードは、concコマンドで実行できます。
$ conc hello hello world 1 hello world 2 hello world 5 hello world 4 hello world 3
並列に実行されるため、実際の出力は上記の例とは異なるかもしれません。
コンパイルと実行という2段階のアプローチとは別に、Concurnas Read-Evaluate-Print-Loop(REPL)を使うこともできます。REPLはまさにJavaのJShellのように動作し、引数のないconcコマンドを実行して起動することができます。
$ conc Welcome to Concurnas 1.14.020 (Java HotSpot(TM) 64-Bit Server VM, Java 11.0.5). Currently running in REPL mode.For help type: /help conc>
これで、次のJVMプロジェクトでConcurnasの力の活用を始める準備が整いました。
まとめ
本記事では、Concurnasプログラミング言語の機能や、Concurnasで最新のCPUおよびGPUハードウェア・アーキテクチャを使用して同時実行パラレル・システムを構築する方法について紹介しました。また、現在特に人気のあるプログラミング・パラダイムに基づいたさまざまなConcurnasの機能を確認し、Concurnasがデータ操作や他のJVM言語で書かれた既存コードの活用に理想的な言語であることを示しました。この言語の詳細を確認したい方は、まずConcurnasのWebサイトを見てみるとよいでしょう。
Jason TattonJason Tatton(@concurnas):Concurnasプログラミング言語の作成者で、Concurnas Ltdの創業者。バンク・オブ・アメリカ、メリルリンチ、ドイツ銀行、J.P.モルガンなど、世界屈指の投資銀行に向けたアルゴリズム取引システムの開発担当およびチーム・リーダーを経験。テクノロジーとプログラミング、そしてConcurnasをできるかぎり最高のプログラミング言語にすることに情熱を燃やしている。 |

