ストリーム#
ストリームはXarpiteのプリミティブなデータ処理のメカニズムの一種である、再利用可能な遅延イテレーションオブジェクトです。
ストリームは本質的には単なる1個の値ですが、非常に多くの場所で複数の値の列であるかのような特殊な扱いを受けます。
ストリームのストリームを作ることは通常の手段では一切不可能で、常に1次元のストリームに平坦化されます。
1要素のストリームとそれの唯一の要素は、基本的に同一視されます。
ストリームの要素列の生成は遅延評価であるため、それに伴う副作用の扱いには特殊な仕様を持ちます。
ストリームの基本#
最も基本的なストリームは、ストリーム結合演算子による要素の列挙です。
CLI版Xarpiteにおいては、ストリームは各要素が1行ずつ出力されます。
$ xa '1, 2, 3'
# 1
# 2
# 3
範囲演算子もまた、特筆すべき基本的なストリームとして非常に多くの場所で使われます。
$ xa '1 .. 3'
# 1
# 2
# 3
パイプ演算子はストリームの各要素を変換する手段として非常に多くの場所で使われます。
$ xa '1 .. 3 | _ * 10'
# 10
# 20
# 30
右実行パイプ演算子はストリーム全体を受け取る関数によって変換する為に、非常に多くの場所で使われます。
$ xa '1 .. 3 >> SUM'
# 6
ストリームの解決#
戻り値としてストリームを返却する演算子や関数は、ストリームの副作用の保証のためにストリームの解決を行う場合があります。
ストリームの解決とは、本来のストリームの代わりに、その要素列を再現するキャッシュのストリームを返すことをいいます。
本来のストリームは解決時に丁度1回だけ全体が評価され、その際に副作用も丁度1回だけ発生します。
この動作は、ストリームという値が本質的に遅延評価される命令の塊であることに起因します。
例えば、以下の例では stream の実際のイテレーションの開始はプログラム全体の結果を実際に出力するまで遅延されます。
$ xa '
array := [1, 2, 3]
stream := array()
array::push << 4, 5
stream
'
# 1
# 2
# 3
# 4
# 5
この性質は、副作用の効果を期待するプログラムでは邪魔になることがあります。
以下の例では、パイプ演算子を使って配列に要素を追加することを期待していますが、2番目の例は副作用が発生しません。
$ xa '
array := [1, 2, 3]
4, 5 | *array::push
array
'
# [1;2;3;4;5]
$ xa '
array := [1, 2, 3]
dummy := 4, 5 | *array::push
array
'
# [1;2;3]
ストリームの解決を行い、副作用をその場で発生させる簡単な方法は、複文によって文(runner)として実行させることです。
$ xa '
array := [1, 2, 3]
dummy := (4, 5 | *array::push;)
array
'
# [1;2;3;4;5]
キャッシュ用の配列の保持は、解決されたストリームの結果がどこにも使われない場合、メモリ節約のために省略される場合があります。
無限ストリームの解決を試みた場合、エラーやリターン演算子等で途中離脱しない限り、プログラムはその場所で無限ループに陥ります。
その結果、プログラムが進行しなくなったり、メモリ不足でクラッシュしたりする可能性があります。
例えば、 LOOP !? "Error" という式は無限ループに陥ります。
これはキャッチ演算子 !? が LOOP に含まれる無限のNULLのどこかで何かがスローされないかチェックしようとするためです。
ストリームの解決を避けるには、ストリームそのものの代わりにストリームを返す関数の状態でやりとりする方法があります。
ただし、この場合キャッチ演算子の効果は関数内には及びません。
$ xa '
getStream := (() -> LOOP | i, _ => i) !? "Error"
getStream() >> TAKE[3]
'
# 0
# 1
# 2
ストリームの結合#
item, ...: ストリーム結合演算子#
ストリーム結合演算子 , は左右の要素またはストリームを結合したストリームを生成します。
Xarpiteでは、ラムダ演算子の引数列のような特殊な場所でない限り、 , は引数や配列要素等の区切りではなくストリーム結合演算子として解釈されます。
$ xa '1, 2 .. 4, 5'
# 1
# 2
# 3
# 4
# 5
ストリーム結合演算子は余計に多く書いても無視されます。
$ xa ', , 1, , , , 2, , '
# 1
# 2
ストリーム結合演算子のみを記述することができ、その場合は空ストリームを生成します。
$ xa '[,]'
# []
xaコマンドはデフォルトの挙動として、与えられたソースコードの戻り値を出力しますが、空のストリームに対しては何も出力しないため、xaコマンドの出力を抑制するのに使われることもあります。
$ xa '"何らかの副作用を伴う処理"; ,'
範囲系演算子#
start .. end: 閉区間演算子#
閉区間演算子は start から end までの整数の範囲のストリームを生成します。
end 自身はストリームに含まれます。
$ xa '1 .. 3'
# 1
# 2
# 3
end が start よりも大きい場合、カウントダウンを行います。
$ xa '3 .. 1'
# 3
# 2
# 1
start と end がともに1文字の文字列である場合、その文字コード範囲のストリームを生成します。
$ xa '"a" .. "c"'
# a
# b
# c
$ xa '"α" .. "γ"'
# α
# β
# γ
start ~ end: 半開区間演算子#
半開区間演算子は start から end の1つ手前までの整数の範囲のストリームを生成します。
end 自身はストリームに含まれません。
$ xa '[1 ~ 3]'
# [1;2]
半開区間演算子は閉区間演算子とは異なり、 end が start よりも大きい場合、空のストリームを生成します。
$ xa '[3 ~ 1]'
# []
start と end がともに1文字の文字列である場合、その文字コード範囲のストリームを生成します。
$ xa '"a" ~ "d"'
# a
# b
# c
$ xa '"α" ~ "δ"'
# α
# β
# γ
ストリームのプロパティアクセス#
ストリームに対してプロパティアクセスをすると、各要素のプロパティアクセスの結果を結合したストリームを返します。
$ xa '
(
{a: 1},
{a: 2 .. 4},
).a
'
# 1
# 2
# 3
# 4
ストリーム系演算子#
ストリーム系演算子は、ストリームの加工や代入などを行う演算子です。
ストリーム系演算子の簡単な紹介#
結合優先度についての解説のため、ストリーム系に属する演算子を軽く紹介します。
パイプ stream | argument => formula は、 stream の各要素について formula を適用したストリームを得ます。
formula 内では、 argument でその要素を参照できます。
$ xa '1, 2, 3 | x => x * 10'
# 10
# 20
# 30
実行パイプ value >> function は、 function に value を渡して実行します。
$ xa '1, 2, 3 >> REVERSE'
# 3
# 2
# 1
変数宣言 variable := value は、変数 variable を宣言しつつ、その値を value で初期化します。
$ xa '
x := 123
x
'
# 123
代入 variable = value は、変数 variable に value を代入します。
$ xa '
x := 123
x = 456
x
'
# 456
結合優先度について#
ストリーム系演算子は、実用上の理由から、以下の文法で表される複雑な結合規則を持っています。
ストリームノード :=
ストリーム結合ノード 代入系演算子 ストリームノード
/ ストリーム結合ノード ストリーム後方付加部*
ストリーム後方付加部 :=
パイプ演算子 パイプ右辺
/ 実行パイプ演算子 実行パイプ右辺
実行パイプ右辺 :=
ストリーム結合ノード 代入系演算子 ストリームノード
/ ストリーム結合ノード
パイプ右辺 :=
ストリーム結合ノード パイプ系演算子 パイプ右辺
/ ストリーム結合ノード 代入系演算子 ストリームノード
/ ストリーム結合ノード
以下では、ストリーム系演算子の文法を例を用いて解説します。
パイプ系演算子は、原則として右優先結合です。
このため、前段の変数を後段から参照することができます。
$ xa '10, 20 | x => 3, 4 | y => x + y'
# 13
# 14
# 23
# 24
$ xa '10, 20 | x => (3, 4 | y => x + y)'
# 13
# 14
# 23
# 24
実行パイプ系演算子は、左側にあるパイプ系・実行パイプ系演算子をまとめて取ります。
これにより、様々に加工したストリームの全体を関数に入力することができます。
$ xa '10, 20 | x => 3, 4 | y => x + y >> JOIN["-"]'
# 13-14-23-24
$ xa '(10, 20 | x => 3, 4 | y => x + y) >> JOIN["-"]'
# 13-14-23-24
実行パイプ系演算子は他の実行パイプ系演算子も左辺にまとめて取ります。
また、実行パイプ系演算子による結果を、さらに別のパイプ系演算子で加工出来ます。
$ xa '1 .. 3 | _ * 10 >> REVERSE | _ + 5 >> JOIN["-"]'
# 35-25-15
$ xa '((1 .. 3 | _ * 10) >> REVERSE | _ + 5) >> JOIN["-"]'
# 35-25-15
代入系演算子は、右辺を左辺から分離します。
代入系演算子の右辺にある実行パイプ系演算子は、代入系演算子の左辺には影響を及ぼしません。
$ xa '
pow2_joiner := stream -> stream | x => x * x >> JOIN["-"]
pow2_joiner(1, 2, 3)
'
# 1-4-9
以下では、関数 setter を呼び出すと、与えた数値に36を足して平方根を取った値を変数 variable に代入します。
SQRT の左にある >> は、その左の = の手前までを左辺に取ります。
$ xa '
variable := NULL
setter := x -> x + 36 | x2 => variable = x2 >> SQRT
setter(64)
variable
'
# 10.0
配列の要素への代入(代入系) array(index) = value#
左辺が配列の要素の参照であった場合、その要素に右辺の値を代入します。
$ xa -q '
array := [1, 2, 3]
OUT << array
array(1) = 4
OUT << array
'
# [1;2;3]
# [1;4;3]
エントリー演算子(代入系) key: value#
エントリー演算子は、両辺を要素とする2要素の配列を生成する演算子です。
$ xa 'a: 1'
# [a;1]
左辺が識別子の場合、同名の変数があっても、変数を参照するのではなく文字列として扱います。
$ xa '
a := "b"
a: 1
'
# [a;1]
左辺で変数を参照したい場合は、括弧で囲むことで文字列として扱われることを回避できます。
$ xa '
a := "b"
(a): 1
'
# [b;1]
エントリー演算子は配列リテラルとは異なり、ストリームを展開せず、常に2要素の配列を生成します。
$ xa '["key"; 1 .. 3]'
# [key;1;2;3]
$ xa 'key: 1 .. 3'
# [key;123]
この演算子はオブジェクトを生成する際に有用です。
$ xa '
{
a: 1
b: 2
}
'
# {a:1;b:2}
左実行パイプ(代入系) function << value#
左実行パイプは、右辺の値を左辺の関数の第1引数に指定して呼び出します。
右実行パイプの左右が逆のバージョンですが、結合優先度が代入系扱いです。
使い方によっては可読性に貢献する可能性を秘めています。
$ xa -q '
OUT << "Hello, World"
'
# Hello, World
パイプ(パイプ系) stream | formula#
パイプ演算子 | は、左辺のストリームの各値に対して右辺を評価し、そのフラットなストリームを返します。
右辺では、変数 _ によって左辺の各要素の値を得ることができます。
$ xa '1 .. 3 | _, _ * 10'
# 1
# 10
# 2
# 20
# 3
# 30
左辺がストリームでない場合、右辺の返却値はストリームで改めてラッピングされることなく、そのままの型で返されます。
$ xa '(5 | _ * 10) + 7'
# 57
右辺に渡される変数は => によって変更できます。
$ xa '5 | x => x * 10'
# 50
右辺の引数を index, value => formula の形式にすることで、左辺のストリームの各要素のインデックスと値を取得できます。
$ xa '"a", "b", "c" | i, v => "$i: $v"'
# 0: a
# 1: b
# 2: c
パイプ演算子をループ構文のように使うこともできます。
$ xa '
x := 0
1 .. 10 | (
x = x + _
)
x
'
# 55
ループ変数は右辺が評価されるごとに独立して作られます。
このため、ループの進行によって変数の内容が変わったり、変数への代入が異なる評価の間で影響し合うことはありません。
$ xa '
accessors := [1 .. 4 | value => {
set: _ -> value = _
get: , -> value
}]
accessors.2.set() = 99
accessors().get()
'
# 1
# 2
# 99
# 4
右実行パイプ(実行パイプ系) value >> function#
右実行パイプは、左辺の値を右辺の関数の第1引数に指定して呼び出します。
$ xa '1 .. 3 >> JOIN["-"]'
# 1-2-3
この演算子はストリームを扱う関数の実行に便利です。
$ xa '"1+2+3" >> SPLIT["+"] | +_ * 2 >> JOIN["-"]'
# 2-4-6
パイプと実行パイプのインデントのベストプラクティス#
パイプ演算子 | と実行パイプ演算子 >> はその左右どちらでも改行でき、ある程度自由に記述できます。
ここではベストプラクティスとして推奨されるインデントスタイルを示します。
パイプの連鎖#
| の位置で改行する場合、その直後もしくはそれに続く => の直後で改行し、右辺をインデントをします。
こうすることで、行頭が常に式の先頭になり、一貫します。
a |
b
a | b =>
c
連鎖する場合も同様です。
a | b =>
c |
d | e =>
f
実行パイプによるパイプのインデントのリセット#
>> の位置で改行する場合、その直前で改行しつつ、それまでの | 演算子によるインデントをすべて解除します。
こうすることで、 | によって宣言されたループ変数のスコープが >> の直前で切れたことが明瞭になります。
a |
b | c =>
d
>> e
>> f
>> の後に | が続く場合、 | の前で改行します。
これにより、 >> のある行のスタイルが一貫します。
a | b =>
c
>> b
>> e
| f =>
g |
h
>> i
ストリーム系関数#
GENERATE: 関数からストリームを生成#
<T> GENERATE(generator: (yield: (item: STREAM<T>) -> NULL) -> NULL): STREAM<T>
generator を実行し、その関数内で yield 関数に渡された item を順番に返すようなストリームを生成します。
$ xa '
GENERATE ( yield =>
yield << 1
yield << 2
yield << 3
)
'
# 1
# 2
# 3
この関数は戻り値のストリームを解決せず、 generator の副作用は GENERATE 関数の戻り値のストリームの評価の都度発生します。
返されるストリームは内部キャッシュを持たないため、安全に無限ストリームを生成することが可能です。
$ xa -q '
stream := GENERATE ( yield =>
OUT << "Called"
)
OUT << "A"
stream
OUT << "B"
stream
OUT << "C"
'
# A
# Called
# B
# Called
# C
item がストリームであった場合、そのストリームは平坦化されて返されます。
$ xa '
GENERATE ( yield =>
yield << 1 .. 3
yield << 4 .. 6
)
'
# 1
# 2
# 3
# 4
# 5
# 6
generator の戻り値がストリームであった場合、そのストリームは1度だけ評価され、その結果1度だけ副作用が発生します。
yield 関数の呼び出しによる要素の出力が「副作用」であることに留意してください。
$ xa '
GENERATE ( yield =>
1 .. 3 | yield << _
)
'
# 1
# 2
# 3
PIPE: 読み取り位置を記憶するストリームを生成する#
<T> PIPE(stream: STREAM<T>): STREAM<T>
stream のイテレーションを保持し、イテレート時に保持した位置から再開するストリームを生成します。
このような性質を持つストリームは、便宜上「パイプ」と呼ばれます。
CLI上で標準入力を受け付ける IN などもパイプに相当します。
パイプは FIRST や TAKE などのストリームを中途半端に消費する関数と組み合わせることで真価を発揮します。
$ xa -q '
pipe := PIPE(1 .. 10)
OUT << "First: " & FIRST(pipe)
OUT << "Next 3 items: " & [pipe >> TAKE[3]]
OUT << "Next: " & FIRST(pipe)
'
# First: 1
# Next 3 items: [2;3;4]
# Next: 5
読み切ったパイプは空ストリームになります。
$ xa -q '
pipe := PIPE(1 .. 10)
OUT << [pipe]
OUT << [pipe]
'
# [1;2;3;4;5;6;7;8;9;10]
# []
性質上、返されるストリームを通じて、 stream は高々1度のみイテレートされます。
これにより stream が引き起こす副作用も複数回発生することはありません。
$ xa '
array := []
pipe := PIPE(
1 .. 3 | (
array::push << _
)
)
pipe
pipe
pipe
array
'
# [1;2;3]
ただし、 PIPE は stream の要素をバッファリングする可能性があり、副作用が意図しないタイミングで発生する可能性があります。
副作用のタイミングを制御するには、副作用のある処理を関数にし、パイプから取り出した側でそれを実行します。
$ xa -q '
tasks := PIPE(
1 .. 10 | , -> (
OUT << "Task $_"
)
)
OUT << "Execute 1 task"
FIRST(tasks) | _()
OUT << "Execute 3 tasks"
TAKE(3; tasks) | _()
OUT << "Execute 1 task"
FIRST(tasks) | _()
'
# Execute 1 task
# Task 1
# Execute 3 tasks
# Task 2
# Task 3
# Task 4
# Execute 1 task
# Task 5
パイプは遅延評価であり、返されたストリームが消費されない場合、 stream のイテレーションは開始されません。
したがって、副作用も発生しません。
$ xa '
array := []
pipe := PIPE(
1 .. 10000 | (
array::push << _
)
)
array
'
# []
stream は無限ストリームであってもかまいません。
パイプが最後まで読み切られなくても、 stream をイテレートするためのリソースはプログラムの終了時に自動的に解放されます。
$ xa '
pipe := PIPE(LOOP | i, _ => i)
OUT << [pipe >> TAKE[10]]
"Finished"
'
# [0;1;2;3;4;5;6;7;8;9]
# Finished
CACHE: ストリームを解決して結果をキャッシュする#
<T> CACHE(stream: STREAM<T>): STREAM<T>
stream を解決し、その結果をキャッシュしたストリームを返します。
VOID 関数と異なり結果を受け取ることができますが、結果のキャッシュのための内部配列にメモリを消費します。
CACHE の呼び出しごとに stream は丁度1回全体がイテレートされ、その際に副作用も丁度1回発生します。
$ xa -q '
stream := 1 .. 3 | OUT << _
OUT << "First"
CACHE(stream)
OUT << "Second"
CACHE(stream)
OUT << "Done"
'
# First
# 1
# 2
# 3
# Second
# 1
# 2
# 3
# Done
一方、 CACHE の戻り値のストリームは何度評価しても副作用が発生しません。
この挙動は配列化と配列のストリーム化のペア [stream]() に相当します。
$ xa -q '
stream := CACHE(1 .. 3 | OUT << _)
OUT << "First"
stream
OUT << "Second"
stream
OUT << "Done"
'
# 1
# 2
# 3
# First
# Second
# Done
CACHE に対して無限ストリームを渡した場合、無限ループと内部配列の無限増加によりプロセスがメモリ不足でクラッシュする可能性があります。
VOID: ストリームを解決して結果を破棄する#
VOID(stream: STREAM): NULL
stream を解決し、その結果を破棄して NULL を返します。
CACHE 関数と異なり結果のキャッシュのための内部配列にメモリを消費しませんが、結果を受け取ることができません。
VOID の呼び出しごとに stream は丁度1回全体がイテレートされ、その際に副作用も丁度1回発生します。
$ xa -q '
stream := 1 .. 3 | OUT << _
OUT << "First"
VOID(stream)
OUT << "Second"
VOID(stream)
OUT << "Done"
'
# First
# 1
# 2
# 3
# Second
# 1
# 2
# 3
# Done
VOID は戻り値がNULLであり、元のストリームとは関係性がありません。
この挙動はストリームを文(runner)コンテキストで実行すること (stream;) に相当します。
$ xa -q '
null := VOID(1 .. 3 | OUT << _)
OUT << "First"
null
OUT << "Second"
null
OUT << "Done"
'
# 1
# 2
# 3
# First
# Second
# Done
VOID に対して無限ストリームを渡した場合、無限ループによりプロセスが応答不能になる可能性があります。