どうも、3 月 5 日を勝手に「世界ビット演算デー」にしたいたぬきです。なんで 3 月 5 日なのかって? $3_{10} = 11_2, 5_{10} = 101_2$ なので並べるだけで真理値表になるからです。ちなみに実際は「軍縮不拡散に関する啓発のための国際デー」らしいです。めちゃシリアスじゃん。
本記事は misskey.dev ユーザー Advent Calendar 2023 の 13 日目の記事でもあります。Qiitadon 爆破事件のあと dev に流れ着いた身なので、まあ Qiita に投げ込んでも問題ないやろ。
misskey.dev にまつわる記事と思いきや、
misskey.dev のユーザーならなんでもどうぞ
とのことなので、好き勝手書きます。私が好きなものといえばビット演算と Excel です。
初期空間を指定できる乱数関数がほしい
Excel には乱数を得る関数が色々ありますが、シード値を設定することができません。モンテカルロ法のシミュレーションで Excel を使うとすると、出た乱数を全部「値としてコピー」しなきゃいけないわけです。複数パターンの検証とか大変ですよね。
じゃあ自分で実装すればいいじゃない。というわけで xorshift 派生アルゴリズムの xoshiro ファミリーを実装していきます。Excel の数値型は(通貨を除いて)おおむね binary64 なので、32 bits 整数範囲は正確に表現できるはずです。よって xoshiro128**を実装します。
VBA ではなく LAMBDA で実装するため、マクロ禁止環境でも利用できます。
ユーティリティ関数ども
まず先に実装の際に便利ないくつかの関数を定義します。
AS32BITS
32 bits 整数範囲になるようマスクする関数です。binary64 では 53 bits までなら正確に表現できますが、今回の要件の場合シフト命令とかで正確に表現されると困るので、この関数を使います。
= LAMBDA(var_n, BITAND(var_n, 4294967295))
BITLSHIFT32
BITLSHIFT
後 AS32BITS
してくれる関数です。それだけなのですが、毎回関数を二つ書くのは面倒なので(あと書き忘れとか怖い)この関数を使います。
= LAMBDA(var_n, var_width, AS32BITS(BITLSHIFT(var_n, var_width)))
BITLROTATE32
BITLSHIFT32
してはみ出たビットを右につけたす、いわゆるローテート演算を行う関数です。ローテートを利用するのが xoshiro ファミリーの特徴です。
= LAMBDA(var_n, var_width, BITOR(BITLSHIFT32(var_n, var_width), BITRSHIFT(var_n, 32 - var_width)))
FLATINDEX
Excel に存在する INDEX
関数は行と列の添字をそれぞれ指定しますが、これを Z 字にたどることを想定して一つの添字で参照するための関数です。
(1, 1) | (1, 2) | (1, 3) |
(2, 1) | (2, 2) | (2, 3) |
(3, 1) | (3, 2) | (3, 3) |
これが公式の INDEX
ですが、これを
1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 |
のように参照できるようにするのが FLATINDEX
です。
SEQUENCE
関数と併用することでこのあとに定義する RESHAPE
関数の実装を容易にします。
= LAMBDA(var_array, var_i, LET(var_cols_num, COLUMNS(var_array), var_row_i, QUOTIENT(var_i - 1, var_cols_num) + 1, var_col_i, MOD(var_i - 1, var_cols_num) + 1, INDEX(var_array, var_row_i, var_col_i)))
これだと処理を追いづらいので整形すると
= LAMBDA(var_array, var_i,
LET(
var_cols_num, COLUMNS(var_array),
var_row_i, QUOTIENT(var_i - 1, var_cols_num) + 1,
var_col_i, MOD(var_i - 1, var_cols_num) + 1,
INDEX(var_array, var_row_i, var_col_i)
)
)
といった感じです。
RESHAPE
配列の形状を要素数が同じ別の形状に変更します。とりあえず 1 列でバーッと乱数を出したあと所望の形状に変更するために使います。
= LAMBDA(var_array, var_rows_num, var_cols_num, FLATINDEX(var_array, SEQUENCE(var_rows_num, var_cols_num)))
状態遷移関数
いよいよ xoshiro128**を実装していきます。といってもリファレンス実装を Excel 数式で書き下していくだけです。ただ、Excel 数式はプログラミング風にいうと宣言的なので、変数の書き換えとかはできません。よって一時変数をふたつ増やして実装しています。
必要な行ぶんの状態を一気に出していきます。繰り返しは使えないので再帰で実装します。1 行だけの場合は与えられた空間をそのまま吐きます。2 行以上の場合は再帰で 1 行前までの状態を吐き、その下に新しい行を追加します。
ところでこの「行を追加する」、HSTACK
/ VSTACK
関数ができてほんとに楽に書けるようになりました。配列リテラルは中身もリテラルじゃないといけない(数式を書けない)問題などがあり、かつては SEQUENCE
関数の結果を IFS
関数で書き換えるみたいなハックが必要でした。
= LAMBDA(var_states_num, var_state_1, var_state_2, var_state_3, var_state_4, IF(var_states_num = 1, HSTACK(AS32BITS(var_state_1), AS32BITS(var_state_2), AS32BITS(var_state_3), AS32BITS(var_state_4)), LET(var_old, XOSHIRO128STATE(var_states_num - 1, var_state_1, var_state_2, var_state_3, var_state_4), var_old_state_1, INDEX(var_old, var_states_num - 1, 1), var_old_state_2, INDEX(var_old, var_states_num - 1, 2), var_old_state_3, INDEX(var_old, var_states_num - 1, 3), var_old_state_4, INDEX(var_old, var_states_num - 1, 4), var_t, BITLSHIFT32(var_old_state_2, 9), var_u, BITXOR(var_old_state_3, var_old_state_1), var_v, BITXOR(var_old_state_4, var_old_state_2), var_new_state_1, BITXOR(var_old_state_1, var_v), var_new_state_2, BITXOR(var_old_state_2, var_u), var_new_state_3, BITXOR(var_u, var_t), var_new_state_4, BITLROTATE32(var_v, 11), var_new, HSTACK(var_new_state_1, var_new_state_2, var_new_state_3, var_new_state_4), VSTACK(var_old, var_new))))
これも整形すると、
= LAMBDA(var_states_num, var_state_1, var_state_2, var_state_3, var_state_4,
IF(var_states_num = 1,
HSTACK(AS32BITS(var_state_1), AS32BITS(var_state_2), AS32BITS(var_state_3), AS32BITS(var_state_4)),
LET(
var_old, XOSHIRO128STATE(var_states_num - 1, var_state_1, var_state_2, var_state_3, var_state_4),
var_old_state_1, INDEX(var_old, var_states_num - 1, 1),
var_old_state_2, INDEX(var_old, var_states_num - 1, 2),
var_old_state_3, INDEX(var_old, var_states_num - 1, 3),
var_old_state_4, INDEX(var_old, var_states_num - 1, 4),
var_t, BITLSHIFT32(var_old_state_2, 9),
var_u, BITXOR(var_old_state_3, var_old_state_1),
var_v, BITXOR(var_old_state_4, var_old_state_2),
var_new_state_1, BITXOR(var_old_state_1, var_v),
var_new_state_2, BITXOR(var_old_state_2, var_u),
var_new_state_3, BITXOR(var_u, var_t),
var_new_state_4, BITLROTATE32(var_v, 11),
var_new, HSTACK(var_new_state_1, var_new_state_2, var_new_state_3, var_new_state_4),
VSTACK(var_old, var_new)
)
)
)
となります。いやだいぶ長いな…
出力関数
次にこれらの内部空間から乱数の出力を得る関数を実装します。xoshiro128**では内部空間のうち 2 番目(1 オリジンで)のみを使います。
= LAMBDA(var_states, LET(var_states_num, ROWS(var_states), var_state_2_s, MAP(SEQUENCE(var_states_num), LAMBDA(row_i, INDEX(var_states, row_i, 2))), AS32BITS(BITLROTATE32(AS32BITS(var_state_2_s * 5), 7) * 9)))
整形すると、
= LAMBDA(var_states,
LET(
var_states_num, ROWS(var_states),
var_state_2_s, MAP(SEQUENCE(var_states_num), LAMBDA(row_i, INDEX(var_states, row_i, 2))),
AS32BITS(BITLROTATE32(AS32BITS(var_state_2_s * 5), 7) * 9)
)
)
となります。
なお、OFFSET
関数を使わず SEQUENCE
関数の戻り値に MAP
関数をかけるというややまだるっこしいことをしていますが、OFFSET
関数は、LET
関数で束縛された式などのような「実際にシート上にあるわけではない配列」に対してエラーを返す場合があるなど、やや動作が非直観的なため、ここでは使用していません。
UI
最後に、ユーザーが実際に使用するための関数を実装します。
= LAMBDA(var_rows_num, var_cols_num, var_seed, LET(var_rands_num, var_rows_num * var_cols_num, var_seed_is_array, ROWS(var_seed) * COLUMNS(var_seed) > 1, var_seed_1, IF(var_seed_is_array, FLATINDEX(var_seed, 1), var_seed), var_seed_2, IF(var_seed_is_array, FLATINDEX(var_seed, 2), MOD(48271 * var_seed_1, 2147483647)), var_seed_3, IF(var_seed_is_array, FLATINDEX(var_seed, 3), MOD(48271 * var_seed_2, 2147483647)), var_seed_4, IF(var_seed_is_array, FLATINDEX(var_seed, 4), MOD(48271 * var_seed_3, 2147483647)), var_states, XOSHIRO128STATE(var_rands_num, var_seed_1, var_seed_2, var_seed_3, var_seed_4), var_rands, XOSHIRO128STARSTAROUTPUT(var_states), RESHAPE(var_rands, var_rows_num, var_cols_num)))
整形します。
= LAMBDA(var_rows_num, var_cols_num, var_seed,
LET(
var_rands_num, var_rows_num * var_cols_num,
var_seed_is_array, ROWS(var_seed) * COLUMNS(var_seed) > 1,
var_seed_1, IF(var_seed_is_array, FLATINDEX(var_seed, 1), var_seed),
var_seed_2, IF(var_seed_is_array, FLATINDEX(var_seed, 2), MOD(48271 * var_seed_1, 2147483647)),
var_seed_3, IF(var_seed_is_array, FLATINDEX(var_seed, 3), MOD(48271 * var_seed_2, 2147483647)),
var_seed_4, IF(var_seed_is_array, FLATINDEX(var_seed, 4), MOD(48271 * var_seed_3, 2147483647)),
var_states, XOSHIRO128STATE(var_rands_num, var_seed_1, var_seed_2, var_seed_3, var_seed_4),
var_rands, XOSHIRO128STARSTAROUTPUT(var_states),
RESHAPE(var_rands, var_rows_num, var_cols_num)
)
)
要求量の乱数を生成して RESHAPE
します。シードはひとつの値でも 4 個(以上)の値を持つ配列でも構いません。ひとつの値を与えた場合、残りの空間は線形合同法で埋めることにしました(正直微妙ではある)。
リファレンス実装の結果と比較。問題なさそうです。
おまけ
ほぎゃーーーーーっ!!
はい、Excel の再帰は割と貧弱なのでそんなにたくさんの乱数は作れません1。だめじゃん。
-
今回の関数の場合 861 個が限界でした。 ↩