アクション言語は複雑なアルゴリズムを記述するというより、入力データと状態の組み合わせに対してデータ変換を行い変換後のデータを後続のシグナルに乗せて発信するという処理が中心となります。
このような処理の記述にはパイプラインのセマンティクスが適しています。
パイプラインを記述するためのプログラミング後の構文としてはUNIXシェルの以下の形式が有力です。
cat file.txt | grep foo | sort
この記述方式は直感的で強力ですが、一般的なプログラミング言語の文法とセマンティクスが異なるため、汎用言語の一要素として併存させることは苦労が伴います。パイプラインで記述できない処理が出てきた時に、パイプライン以外の方式で記述した上で、それをパイプラインにどのように簡潔な形で統合していくのかという点が問題です。煩雑な記述方式で統合すると、"直感的で強力"な文法が失われてしまいます。
この問題への対応としてKaleidoxではスタックのメカニズムを用意しています。スタックを用いることで、パイプラインのセマンティクスに近い形で通常のプログラミング言語との"直感的で強力"な統合を目指しています。
スタック
式の評価結果はスタックにプッシュされます。
たとえばREPLから1、2、3と順に入力するとそれぞれの式の評価結果である数値がスタックにプッシュされていきます。
kaleidox> 1 1 kaleidox> 2 2 kaleidox> 3 3
スタックの様子はstackコマンドで表示することができます。先程入力した数値がスタック上に逆順で積まれていることが確認できました。
kaleidox> #stack 1: 3 2: 2 3: 1 ...略...
関数は必須引数数が定義されており、引数の数がこれに満たない場合は不足分をスタックから持ってくるようになっています。
前述のスタックの状態の元で関数+を引数なしで投入してみます。+関数の必須引数の数は2なので、スタックから2つの値をポップし、これを引数として利用します。
kaleidox> + 6
+関数実行後のスタックは以下になります。
kaleidox> #stack 1: 1 ...略...
例
より実用的な例としてCSVデータをチャートで表示する処理を考えてみます。
以下のCSVデータをcity.csvとして用意します。
都市,緯度,経度,平均気温,降水量 札幌,43.055248,141.345505,8.0,1158 仙台,38.254162,140.891403,11.9,1219 東京,35.680909,139.767372,15.3,1460 名古屋,35.154919,136.920593,14.9,1575 大阪,34.702509,135.496505,16.2,1400 広島,34.377560,132.444794,15.0,1603 福岡,33.579788,130.402405,16.0,1690 那覇,26.204830,127.692398,22.4,2128
table-load関数でCSVデータをtableオブジェクトとして読み込みます。
kaleidox> table-load file:city.csv Table[5x8] kaleidox> :show:print ┏━━━━━━┯━━━━━━━━━┯━━━━━━━━━━┯━━━━━━━━┯━━━━━━┓ ┃都市 │緯度 │経度 │平均気温│降水量┃ ┣━━━━━━┿━━━━━━━━━┿━━━━━━━━━━┿━━━━━━━━┿━━━━━━┫ ┃札幌 │43.055248│141.345505│8.0 │1158 ┃ ┃仙台 │38.254162│140.891403│11.9 │1219 ┃ ┃東京 │35.680909│139.767372│15.3 │1460 ┃ ┃名古屋│35.154919│136.920593│14.9 │1575 ┃ ┃大阪 │34.702509│135.496505│16.2 │1400 ┃ ┃広島 │34.377560│132.444794│15.0 │1603 ┃ ┃福岡 │33.579788│130.402405│16.0 │1690 ┃ ┃那覇 │26.204830│127.692398│22.4 │2128 ┃ ┗━━━━━━┷━━━━━━━━━┷━━━━━━━━━━┷━━━━━━━━┷━━━━━━┛
続けて、table-select関数を使って読み込んだ表の必要部分だけ切り取ります。ここでは第0、1、3カラムを残し、都市、緯度、平均気温の表にしています。
kaleidox> table-select 0,1,3 Table[3x8] kaleidox> :show:print Table[3x8] ┏━━━━━━┯━━━━━━━━━┯━━━━━━━━┓ ┃都市 │緯度 │平均気温┃ ┣━━━━━━┿━━━━━━━━━┿━━━━━━━━┫ ┃札幌 │43.055248│8.0 ┃ ┃仙台 │38.254162│11.9 ┃ ┃東京 │35.680909│15.3 ┃ ┃名古屋│35.154919│14.9 ┃ ┃大阪 │34.702509│16.2 ┃ ┃広島 │34.377560│15.0 ┃ ┃福岡 │33.579788│16.0 ┃ ┃那覇 │26.204830│22.4 ┃ ┗━━━━━━┷━━━━━━━━━┷━━━━━━━━┛
table-select関数は第1引数に選択するカラムを示す範囲を第2引数に対象のtableオブジェクトを取ります。第1引数のみ指定されてるので、第2引数はスタックよりtable-load関数の評価結果であるcity.csvを読み込んだtableオブジェクトが指定されます。
続けてtable-chart関数を使ってチャートの表示を行います。
kaleidox> table-chart :analyzes 'simple-regression
table-chart関数ではキーワードanalyzesにsimple-regressionを指定しているので、散布図の上に単回帰分析の結果が表示されます。キーワードの後ろに指定する引数ではtableオブジェクトを取ります。ここが省略されているので、スタックよりtable-select関数の評価結果であるカラム射影後のtableオブジェクトが指定されます。
この結果、以下のチャートが表示されます。
関数
ここまでREPLで試してきた処理では以下の3つの関数を順に実行しています。
- table-load
- table-select
- table-chart
これは前出のUNIXシェルのパイプラインの記述方式を使うと以下のようなイメージです。
table-load file:csv | table-select 0,1,3 | table-chart :analyzes 'simple-regression
この処理を通常のLisp文法で関数化したものが以下のshowchart関数です。
(defun showchart (uri) (setq a (table-load uri)) (setq b (table-select 0,1,3) a) (table-chart :analyzes 'simple-regression b))
前段の処理結果を後段に受け渡していくパイプライン的な処理ですが、この例でも分かるように手続き型的な記述方式では変数を使ってデータを受け渡していく形になります。
この記述方式でも実用的には問題ありませんが、UNIXシェルの記述方式に比べると煩雑であることは否めません。
KaleidoxではUNIXシェルのような記述はできませんが、関数呼び出しをシーケンシャルに並べてパイプライン的な処理を簡潔に記述することができます。
この記述を用いて関数化したものが以下のshowchart関数です。
(defun showchart (uri) (table-load uri) (table-select 0,1,3) (table-chart :analyzes 'simple-regression))
table-load, table-select, table-chartのそれぞれの評価結果をスタックを用いて後続の関数に受け渡しています。
スタックを用いてこのような記述方式を可能にすることで、パイプライン的な処理を簡潔に記述することが可能になっています。
まとめ
アクション言語は、汎用言語としての記述力も重要ですが、処理の中心はデータ変換のパイプライン処理になるので、パイプライン処理を簡潔に記述する記述能力も重要になります。
Kaleidoxではスタックを用いてパイプライン処理を簡潔に記述することを可能にしているわけです。
関数型言語ではFunctor(Scalaではmap関数)やMonad(ScalaではflatMap関数)を用いてパイプライン処理を記述することが可能になっています。この関数型言語方式もパイプラインの記述方式としては有力、というか汎用言語としては本命と思いますが、アクション言語にはやや重いのではないかというのがボクの見立てで、スタックによる記述方式を開発しました。
関数型言語方式とスタック方式の比較検討はいずれ行いたいと思います。
諸元
- Kaleidox : 0.1.11