最近プリプロセス時処理が楽しい

はじめに

Boost.Preprocessorの実装を調べているうちに色々出来ることが分かって楽しくなってきた。

基本的なことから曲芸的なことまで適当にパターンをメモっておきます。

シンボルの結合

#define PP_CAT(a, b) PP_CAT_I(a, b)
#define PP_CAT_I(a, b) a ## b

#define PP_CAT3(a, b, c) PP_CAT3_I(a, b, c)
#define PP_CAT3_I(a, b, c) a ## b ## c

#define X x
#define Y y
#define Z z

PP_CAT(a, b) // => ab
PP_CAT(X, Y) // => xy

PP_CAT3(a, b, c) // => abc
PP_CAT3(X, Y, Z) // => xyz

論理演算

数値 => 0/1

#define PP_BOOL(x) PP_CAT(PP_BOOL_, x)
#define PP_BOOL_0  0
#define PP_BOOL_1  1
#define PP_BOOL_2  1
#define PP_BOOL_3  1
#define PP_BOOL_4  1
// ...

PP_BOOL(0) // => 0
PP_BOOL(1) // => 1
PP_BOOL(2) // => 1
PP_BOOL(3) // => 1

否定

#define PP_NOT(x) PP_CAT(PP_NOT_, PP_BOOL(x))
#define PP_NOT_0 1
#define PP_NOT_1 0

PP_NOT(0) // => 1
PP_NOT(1) // => 0
PP_NOT(2) // => 0
PP_NOT(3) // => 0

AND/OR

#define PP_OR(x, y) PP_CAT3(PP_OR_, x, y)
#define PP_OR_00 0
#define PP_OR_01 1
#define PP_OR_10 1
#define PP_OR_11 1

PP_OR(0, 0) // => 0
PP_OR(0, 1) // => 1
PP_OR(1, 0) // => 1
PP_OR(1, 1) // => 1

#define PP_AND(x, y) PP_CAT3(PP_AND_, x, y)
#define PP_AND_00 0
#define PP_AND_01 0
#define PP_AND_10 0
#define PP_AND_11 1

PP_AND(0, 0) // => 0
PP_AND(0, 1) // => 0
PP_AND(1, 0) // => 0
PP_AND(1, 1) // => 1

条件分岐

#define PP_IF(b, x, y) PP_CAT(PP_IF_, PP_BOOL(b))(x, y)
#define PP_IF_0(x, y) y
#define PP_IF_1(x, y) x

PP_IF(0, a, b) // => b
PP_IF(1, a, b) // => a

引数

先頭/先頭以外の引数の取り出し

#define PP_ARG_HEAD(_0, ...) _0
#define PP_ARG_TAIL(_0, ...) __VA_ARGS__

PP_ARG_HEAD(a, b, c) // => a
PP_ARG_TAIL(a, b, c) // => b, c

N個目の引数の取り出し

#define PP_ARG_GET(n, ...) PP_CAT(PP_ARG_GET_, n)(__VA_ARGS__)
#define PP_ARG_GET_0(_0, ...) _0
#define PP_ARG_GET_1(_0, _1, ...) _1
#define PP_ARG_GET_2(_0, _1, _2, ...) _2
#define PP_ARG_GET_3(_0, _1, _2, _3, ...) _3
...

PP_ARG_GET(0, a, b, c) // => a
PP_ARG_GET(1, a, b, c) // => b
PP_ARG_GET(2, a, b, c) // => c

引数の個数

#define PP_ARG_SIZE(...) PP_ARG_GET_11(, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

PP_ARG_SIZE() // => 0
PP_ARG_SIZE(a) // => 1
PP_ARG_SIZE(a, b) // => 2
PP_ARG_SIZE(a, b, c) // => 3

この例では10個までの引数を数えられます。
##__VA_ARGS__gcc, clangの拡張です。C++20には __VA_OPT__ が入るので拡張に頼る必要がなくなります。

リスト操作

展開

#define PP_LIST_UNPACK(list) PP_LIST_UNPACK_I list
#define PP_LIST_UNPACK_I(...) __VA_ARGS__

PP_LIST_UNPACK((a, b, c)) // => a, b, c

先頭の要素、先頭を取り除いたリストの取り出し

#define PP_LIST_HEAD(list) PP_ARG_HEAD list
#define PP_LIST_TAIL(list) (PP_ARG_TAIL list)

PP_LIST_HEAD((a, b, c)) // => a
PP_LIST_TAIL((a, b, c)) // => (b, c)

N番目の要素の取り出し

#define PP_LIST_GET(list, n) PP_CAT(PP_ARG_GET_, n) list

PP_LIST_GET((a, b, c), 0) // => a
PP_LIST_GET((a, b, c), 1) // => b
PP_LIST_GET((a, b, c), 2) // => c

要素の個数

#define PP_LIST_SIZE(list) PP_ARG_SIZE list

PP_LIST_SIZE(()) // => 0
PP_LIST_SIZE((a)) // => 1
PP_LIST_SIZE((a, b)) // => 2
PP_LIST_SIZE((a, b, c)) // => 3

リストが空かどうか

#define PP_LIST_IS_EMPTY(list) PP_NOT(PP_LIST_SIZE(list))

PP_LIST_IS_EMPTY(()) // => 1
PP_LIST_IS_EMPTY((a)) // => 0
PP_LIST_IS_EMPTY((a, b)) // => 0
PP_LIST_IS_EMPTY((a, b, c)) // => 0

要素の追加

#define PP_LIST_PUSH_FRONT(list, item) PP_IF(PP_LIST_SIZE(list), (item, PP_LIST_UNPACK(list)), (item))
#define PP_LIST_PUSH_BACK(list, item) PP_IF(PP_LIST_SIZE(list), (PP_LIST_UNPACK(list), item), (item))

PP_LIST_PUSH_FRONT((), a) // => (a)
PP_LIST_PUSH_FRONT((b), a) // => (a, b)
PP_LIST_PUSH_FRONT((b, c), a) // => (a, b, c)

PP_LIST_PUSH_BACK((), a) // => (a)
PP_LIST_PUSH_BACK((a), c) // => (a, b)
PP_LIST_PUSH_BACK((a, b), c) // => (a, b, c)

繰り返し

#define PP_WHILE(pred, op, state) PP_WHILE_0(pred, op, state)
#define PP_WHILE_0(pred, op, state) PP_IF(pred(state), PP_WHILE_1(pred, op, op(state)), state)
#define PP_WHILE_1(pred, op, state) PP_IF(pred(state), PP_WHILE_2(pred, op, op(state)), state)
#define PP_WHILE_2(pred, op, state) PP_IF(pred(state), PP_WHILE_3(pred, op, op(state)), state)
...
#define PP_WHILE_10(pred, op, state) PP_IF(pred(state), PP_WHILE_11(pred, op, op(state)), state)
#define PP_WHILE_11(pred, op, state) PP_WHILE_ERROR

PP_WHILE(PP_LIST_SIZE, PP_LIST_TAIL, (a, b, c)) // => ()
PP_WHILE(PP_LIST_IS_EMPTY, PP_LIST_TAIL, (a, b, c)) // => (a, b, c)

pred(state) が1の間 state <- op(state) を適用し続けます。

擬似コードで書くとこんな感じ

function PP_WHILE(pred, op, state) {
	while (pred(state))
		state = op(state)

	return state
}

ただしマクロ関数は再帰呼び出しが出来ないため同じマクロ関数を複数用意して順番に呼び出していくことで擬似的にループを実現します。
この例では10回までのイテレーションが可能です。

また、PP_WHILE()の引数predまたはopPP_WHILE()を内部で使うマクロ関数は渡せません。
その場合は再帰呼び出しになるためマクロ展開が失敗します。
そのため、PP_WHILE()と同様のマクロ関数を複数用意しておくといいでしょう。

#define PP_WHILE_A(pred, op, state) PP_WHILE_A_0(pred, op, state)
#define PP_WHILE_A_0(pred, op, state) PP_IF(pred(state), PP_WHILE_A_1(pred, op, op(state)), state)
#define PP_WHILE_A_1(pred, op, state) PP_IF(pred(state), PP_WHILE_A_2(pred, op, op(state)), state)
#define PP_WHILE_A_2(pred, op, state) PP_IF(pred(state), PP_WHILE_A_3(pred, op, op(state)), state)
...
#define PP_WHILE_A_10(pred, op, state) PP_IF(pred(state), PP_WHILE_A_11(pred, op, op(state)), state)
#define PP_WHILE_A_11(pred, op, state) PP_WHILE_ERROR

#define PP_WHILE_B(pred, op, state) PP_WHILE_B_0(pred, op, state)
#define PP_WHILE_B_0(pred, op, state) PP_IF(pred(state), PP_WHILE_B_1(pred, op, op(state)), state)
#define PP_WHILE_B_1(pred, op, state) PP_IF(pred(state), PP_WHILE_B_2(pred, op, op(state)), state)
#define PP_WHILE_B_2(pred, op, state) PP_IF(pred(state), PP_WHILE_B_3(pred, op, op(state)), state)
...
#define PP_WHILE_B_10(pred, op, state) PP_IF(pred(state), PP_WHILE_B_11(pred, op, op(state)), state)
#define PP_WHILE_B_11(pred, op, state) PP_WHILE_ERROR

#define PP_WHILE_C(pred, op, state) PP_WHILE_C_0(pred, op, state)
#define PP_WHILE_C_0(pred, op, state) PP_IF(pred(state), PP_WHILE_C_1(pred, op, op(state)), state)
#define PP_WHILE_C_1(pred, op, state) PP_IF(pred(state), PP_WHILE_C_2(pred, op, op(state)), state)
#define PP_WHILE_C_2(pred, op, state) PP_IF(pred(state), PP_WHILE_C_3(pred, op, op(state)), state)
...
#define PP_WHILE_C_10(pred, op, state) PP_IF(pred(state), PP_WHILE_C_11(pred, op, op(state)), state)
#define PP_WHILE_C_11(pred, op, state) PP_WHILE_ERROR
[追記]

BOOST_PP_AUTO_REC()(AUTO RECursion)のような仕組みを用いることで使用すべきPP_WHILE_*を自動的に決定できるためPP_WHILE()の引数にPP_WHILE()を使う関数を渡せるようにできます。

www.slideshare.net

#define PP_AUTO_REC(pred) PP_AUTO_REC_NODE_2(pred)

#define PP_AUTO_REC_NODE_2(p) PP_IF(p(2), PP_AUTO_REC_NODE_1(p), PP_AUTO_REC_NODE_3(p))
#	define PP_AUTO_REC_NODE_1(p) PP_IF(p(1), 1, 2)
#	define PP_AUTO_REC_NODE_3(p) PP_IF(p(3), 3, 4)

#define PP_WHILE PP_CAT(PP_WHILE, PP_AUTO_REC(PP_WHILE_CHECK))
#define PP_WHILE_CHECK(n) PP_CAT(PP_WHILE_CHECK_, PP_WHILE ## n(PP_WHILE_CHECK_P, PP_WHILE_CHECK_O, OK))
#define PP_WHILE_CHECK_P(s) 0
#define PP_WHILE_CHECK_O(s)
#define PP_WHILE_CHECK_OK 1

#define PP_WHILE1 PP_WHILE1_0
#define PP_WHILE_CHECK_PP_WHILE1_0(p, o, s) 0
#define PP_WHILE1_0(pred, op, state) PP_IF(pred(state), PP_WHILE1_1(pred, op, op(state)), state)
#define PP_WHILE1_1(pred, op, state) PP_IF(pred(state), PP_WHILE1_2(pred, op, op(state)), state)
...

#define PP_WHILE2 PP_WHILE2_0
#define PP_WHILE_CHECK_PP_WHILE2_0(p, o, s) 0
#define PP_WHILE2_0(pred, op, state) PP_IF(pred(state), PP_WHILE2_1(pred, op, op(state)), state)
#define PP_WHILE2_1(pred, op, state) PP_IF(pred(state), PP_WHILE2_2(pred, op, op(state)), state)
...

#define PP_WHILE3 PP_WHILE3_0
#define PP_WHILE_CHECK_PP_WHILE3_0(p, o, s) 0
#define PP_WHILE3_0(pred, op, state) PP_IF(pred(state), PP_WHILE3_1(pred, op, op(state)), state)
#define PP_WHILE3_1(pred, op, state) PP_IF(pred(state), PP_WHILE3_2(pred, op, op(state)), state)
...

#define PP_WHILE4 PP_WHILE4_0
#define PP_WHILE_CHECK_PP_WHILE4_0(p, o, s) 0
#define PP_WHILE4_0(pred, op, state) PP_IF(pred(state), PP_WHILE4_1(pred, op, op(state)), state)
#define PP_WHILE4_1(pred, op, state) PP_IF(pred(state), PP_WHILE4_2(pred, op, op(state)), state)
...

算術演算

インクリメント/デクリメント

#define PP_INC(x) PP_CAT(PP_INC_, x)
#define PP_INC_0  1
#define PP_INC_1  2
#define PP_INC_2  3
...
#define PP_INC_9  10
#define PP_INC_10 10

#define PP_DEC(x) PP_CAT(PP_DEC_, x)
#define PP_DEC_0  0
#define PP_DEC_1  0
#define PP_DEC_2  1
...
#define PP_DEC_9  8
#define PP_DEC_10 9

PP_INC(0) // => 1
PP_INC(1) // => 2
PP_INC(2) // => 3
PP_INC(10) // => 10

PP_DEC(0) // => 0
PP_DEC(1) // => 0
PP_DEC(2) // => 1
PP_DEC(10) // => 9

結果の値が[0, N]の範囲にクランプされてたほうが扱いやすいのでPP_INC(10) == 10, PP_DEC(0) == 0になるようにしています。

加算

PP_WHILE()PP_INC(), PP_DEC()を用いて繰り返し数値をインクリメントすることで実現します。

#define PP_ADD(x, y) PP_LIST_GET(PP_WHILE(PP_LIST_HEAD, PP_ADD_O, (x, y)), 1)
#define PP_ADD_O(xy) (PP_DEC(PP_LIST_GET(xy, 0)), PP_INC(PP_LIST_GET(xy, 1)))

PP_ADD(2, 3) // => 5
PP_ADD(5, 2) // => 7

結果は[0, 10]の範囲に丸められます。

PP_WHILE()を内部で使用するため、別のPP_WHILE_*を使う同様の関数を用意しておくといいです。

#define PP_ADD_A(x, y) PP_LIST_GET(PP_WHILE_A(PP_LIST_HEAD, PP_ADD_O, (x, y)), 1)
#define PP_ADD_B(x, y) PP_LIST_GET(PP_WHILE_B(PP_LIST_HEAD, PP_ADD_O, (x, y)), 1)
#define PP_ADD_C(x, y) PP_LIST_GET(PP_WHILE_C(PP_LIST_HEAD, PP_ADD_O, (x, y)), 1)

減算

#define PP_SUB(x, y) PP_LIST_GET(PP_WHILE(PP_LIST_HEAD, PP_SUB_O, (y, x)), 1)
#define PP_SUB_O(xy) (PP_DEC(PP_LIST_GET(xy, 0)), PP_DEC(PP_LIST_GET(xy, 1)))

PP_SUB(2, 3) // => 0
PP_SUB(5, 2) // => 3

#define PP_SUB_A(x, y) PP_LIST_GET(PP_WHILE_A(PP_LIST_HEAD, PP_SUB_O, (y, x)), 1)
#define PP_SUB_B(x, y) PP_LIST_GET(PP_WHILE_B(PP_LIST_HEAD, PP_SUB_O, (y, x)), 1)
#define PP_SUB_C(x, y) PP_LIST_GET(PP_WHILE_C(PP_LIST_HEAD, PP_SUB_O, (y, x)), 1)

結果は[0, 10]の範囲に丸められます。

同様に繰り返し加減算を行うことで乗算や除算、剰余なども実装できますが割愛します。

さて、これで条件分岐、繰り返しと加減算が扱えるようになったので色々なものが書けます。

(0, 1, 2, ..., n-1)のようなリストを生成したり、

#define PP_IOTA(n) PP_LIST_GET(PP_WHILE(PP_LIST_HEAD, PP_IOTA_O, (n, ())), 1)
#define PP_IOTA_O(nl) (PP_DEC(PP_LIST_GET(nl, 0)), PP_LIST_PUSH_FRONT(PP_LIST_GET(nl, 1), PP_DEC(PP_LIST_GET(nl, 0))))

PP_IOTA(0) // => ()
PP_IOTA(3) // => (0, 1, 2)
PP_IOTA(10) // => (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

リストを逆順にしたり、

#define PP_REVERSE(list) PP_LIST_GET(PP_WHILE(PP_REVERSE_P, PP_REVERSE_O, (list, ())), 1)
#define PP_REVERSE_P(io) PP_LIST_SIZE(PP_LIST_GET(io, 0))
#define PP_REVERSE_O(io) (PP_LIST_TAIL(PP_LIST_GET(io, 0)), PP_LIST_PUSH_FRONT(PP_LIST_GET(io, 1), PP_LIST_HEAD(PP_LIST_GET(io, 0))))

PP_REVERSE(PP_IOTA(5)) // => (4, 3, 2, 1, 0)

リストの各要素に関数を適用したり。

#define PP_MAP(list, f) PP_LIST_GET(PP_WHILE(PP_MAP_P, PP_MAP_O, (list, f, ())), 2)
#define PP_MAP_P(ifo) PP_LIST_SIZE(PP_LIST_GET(ifo, 0))
#define PP_MAP_O(ifo) PP_MAP_O_I ifo
#define PP_MAP_O_I(in, f, out) (PP_LIST_TAIL(in), f, PP_LIST_PUSH_BACK(out, f(PP_LIST_HEAD(in))))

#define DOUBLE(x) PP_ADD_A(x, x)
PP_MAP(PP_IOTA(4), DOUBLE) // => (0, 2, 4, 6, 8)

#define ADD_PREFIX_n(x) PP_CAT(n, x)
int PP_LIST_UNPACK(PP_MAP(PP_IOTA(3), ADD_PREFIX_n)); // => int n0, n1, n2;

最後の例とかC++03書く時とか便利だったりするんじゃないでしょうか。

まとめ

以上全コード
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ


プリプロセッサでも様々なことが出来るのがわかったと思います。
手触りとしては再帰呼び出しと算術演算の出来ないSchemeって感じです。
試してみると結構楽しいのでおすすめです(?)

コードはまだ洗練できていませんがFizz Buzzとクイックソートを実装したので供養しておきます。

月曜日の空飛ぶオレンジ

この記事は まんがタイムきらら Advent Calendar 2017 - Adventar の17日目の記事です。

www.adventar.org

はじめに

今年も半月を残し、師走の慌ただしさを感じる頃合いになりました。

きららファンタジアのサービスも開始し、さらに来る年の頭には待ちに待った ゆるキャン△ と、なにより スロウスタート のアニメが放送されます。

たまてちゃんのツインテールがふよんふよん動く様が楽しみです。

さて、完結済みの作品には素晴らしい作品が多く存在しますがそれらに関する情報を目にする機会は、当然掲載中の作品に関するそれと比べ少ないです。

というわけで、折角の機会なので自分の好きな完結済み作品の話をしたいと思います。

月曜日の空飛ぶオレンジ

月曜日の空飛ぶオレンジ。 1巻 (まんがタイムKRコミックス)

月曜日の空飛ぶオレンジ。 2巻 (まんがタイムKRコミックス)

情報

著者: あfろ

掲載誌: まんがタイムミラク (vol.1 ~ 平成25年7月号)

巻数: 全2巻

紹介

スイカの自販機。

朝起きたら頭が箱に。

植木鉢に信号機。

キュート&フリーダムなこの興奮、初体験。

Amazon - 月曜日の空飛ぶオレンジ (1) 作品内容より

月曜日の空飛ぶオレンジ は現在 ゆるキャン△ (きららフォワード) と mono (ミラクからキャラットへ移籍) を連載されている あfろ先生 の前々作です。

この作品はいわゆる不条理系です。

時速100kmのスイカ、上から降ってくる金ダライ、タイムトラベルするカレーライス。

架空の離島、六日島を舞台に女子高生と 犬と 謎のオレンジ色の生物つかぽんによる不思議な日常を描いたマンガです。

正直なところ初めてこれを読んだときには、異様な世界観になんだこれと半ば首を捻りながら読んでいましたが、 この雰囲気に慣れる頃にはテンポの良い畳み掛けるようなギャグが段々と癖になっていきます。

かわいらしい絵柄とパースを効かせた背景、犬、バイク……。あfろ先生らしさに溢れた癖の強い作品です。

数式微分器の作成 (2) 式木の最適化

前回の記事の続きです。

数式微分器の作成 と D言語での木走査 - 茅の下

前回の内容

前回の記事で数式微分器を作成したが、出力が繁雑になった。

結論

今回はそれを解消するために式木に対してより強力な最適化を掛けるようにした結果、出力がこましになった。

前回の反省

前回、出力が繁雑になった原因は主に次の2つです。

  1. 割り算ノードを用意しなかった。

    前回は必要な式木の型数を小さくするために、割り算に相当するノード型を用意しませんでした。

    そのため、{ f / g } f \times g^{-1} のように表現する必要があり、これが出力の式木のノード数が増える原因になっていました。

    今回は割り算ノードを用意しました。

  2. 式木に対する最適化が甘かった。

    前回式木に対して施していた最適化は以下の3項目のみでした。

    1. 加算両辺の 0 の削除。

       0 + b → b

       a + 0 → a

    2. 乗算両辺の 1 の削除。

       1 \times b → b

       a \times 1 → a

    3. 0 を含む乗算の削除。

       0 \times b → 0

       a \times 0 → 0

    今回はこれらに加えて式木に対して様々な最適化を行いました。

GitHub - Ryooooooga/Calculator

結果

では前回と同様、 1 / tan(x) (= cos(x) \times sin(x)^{-1})微分してみます。

以下が前回の出力結果です。

((((-1)*sin(x))*sin(x)^{(-1)})+(cos(x)*(sin(x)^{(-1)}*((cos(x)*(-1))*sin(x)^{(-1)}))))

以下が今回の最適化を施したあとの出力です。

(-1-((cos(x)^2)/(sin(x)^2)))

依然として括弧だらけですが、全体的にこざっぱりしました。

なお、二階微分すると爆発します。

-(-(((((((cos(x)^2)*(2*sin(x)))*(sin(x)^2))/cos(x))+(((cos(x)^2)*((sin(x)^2)*(2*cos(x))))/sin(x)))/((sin(x)^2)^2))))

はいふり 人称行列

はいふりのキャラクター間の一、二、三人称を纏めています。

docs.google.com

現在本編とOVA前編・後編、加えてローレライの乙女たち3話まで完了しています。

収集の条件は大まかに、

  • 呼び掛ける側が呼ばれる側の名前を認識していること。

    (例えば、2話気絶しているミーナに対して岬が呼び掛けた「『あなた』、生きてるよ」などはこの時点で岬がミーナの名前を認識していないため含みません)

  • 呼び掛ける側が呼ばれる側の個人を特定していること。

    (『あなたたち』の様に呼ばれる側が複数形の特定されないものなどは含みません)

の二つです。ただし、直前の会話文との重複を避けるために用いられる『あなた』などは例外的に含まないものとします。

また、特別な状況下での呼び掛け(幼少期の ましろ から真霜に対する『お姉ちゃん』など)の場合は可能な限りその話数を併記します。

何かの役に立てばいいです。

数式微分器の作成 と D言語での木走査

モチベーション

無性に微分がしたくなった。

結論

出力が頭悪い。

D言語と振り分け式の外部訪問器は相性がいい。

特に数学的におもしろい話はありません。

はじめに

偏導関数値を求める方法には、数値微分 や (トップダウン/ボトムアップ)型自動微分などがありますが、今回は具体的な値を求めたいわけではないので数式微分を行います。

方法

式木で表される関数を、再帰的に微分することで偏微分関数を表す式木を作ります。

まず、ある程度簡単な関数であれば、三角関数のようなより単純な関数の組み合わせや四則演算で表すことが出来ます。

それらの 関数の組み合わせ に対する微分結果の式木を求めることで微分を行います。

例えば、{ y(x) = f(x) + g(x) } という関数の場合、

 y' = f' + g'

というふうに偏導関数を求めることが出来ます。以下に幾つかの関数の組み合わせに対応する微分結果を挙げます。

微分 微分結果
加算:  y = f + g    y' = f'+g'
乗算:  y = fg       y' = f'g + fg'
正弦:  y = sin(f)   y' = f'cos(f)
余弦:  y = cos(f)   y' = -f'sin(f)
冪乗:  y = f^{g}    y' = f^{g}(g'log(f)+\frac{f'}{f} g)
対数:  y = log(f)   y' = \frac{f'}{f}

このような規則に従い式木を走査し組み直すプログラムをD言語で作成しました。

https://github.com/Ryooooooga/DifferentialCulculator

では試しに  1/tan(x)微分してみます。

出力は以下の様になります。

((((-1)*sin(x))*sin(x)^{(-1)})+(cos(x)*(sin(x)^{(-1)}*((cos(x)*(-1))*sin(x)^{(-1)}))))

はい、出力の整形を面倒くさがったため括弧地獄になっています。

これを適当に手動で整形すると、

 -\frac{sin(x)}{sin(x)} - cos(x)\frac{\frac{cos(x)}{sin(x)}}{sin(x)}

の様になり、 1/tan(x)微分結果である

 -1 - \frac{cos^{2}(x)}{sin^{2}(x)}

と一致することがわかります。

まとめ

一応式木を走査して最低限無駄な項を切り落とすようにはしているのですが、出力結果が見れたものではないので、出力の式木に対してより高度な最適化を施す必要がありそうです。

木構造の走査と戦略

デザインパターンっぽい話です。

式木や構文木のような木構造の走査の戦略にはいくつかのパターンがあり、それぞれにメリット、デメリットが存在します。

下に、よく使われる3つのパターンを挙げます。

また、それぞれに例として簡単なC#ソースコードを載せます。

1. 走査メソッド(Interpreter パターン)

最も直感的なパターンです。

式木のノードを表すクラスが走査メソッドを持ち、再帰的に子ノードのメソッドを呼び出すことで木を走査します。

// ノード
interface INode {
    // 走査メソッド
    double Eval();
}

// 加算
class Add: INode {
    private INode left, right;
    
    public Add(INode left, INode right) {
        this.left  = left;
        this.right = right;
    }
    
    // 走査メソッドの実装
    public double Eval() => this.left.Eval() + this.right.Eval();
}

// 乗算
class Mul: INode {
    private INode left, right;
    
    public Mul(INode left, INode right) {
        this.left  = left;
        this.right = right;
    }
    
    // 走査メソッドの実装
    public double Eval() => this.left.Eval() * this.right.Eval();
}

// 数値
class Num: INode {
    private double value;
    
    public Num(double value) {
        this.value = value;
    }
    
    // 走査メソッドの実装
    public double Eval() => this.value;
}

class Prog {
    public static void Main() {
        // 1*2 + 3*4
        var tree = new Add(
            new Mul(new Num(1), new Num(2)),
            new Mul(new Num(3), new Num(4))
        );
        
        System.Console.WriteLine(tree.Eval());
    }
}

実行結果

14

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

上の例では、「加算」、「乗算」、「数値」というノードクラスがそれぞれ Eval メソッドを実装することで木の評価を行っています。

メリット

  1. わかりやすい

    仕組みが単純で動作がわかりやすいです。

  2. 引数をとれる、返り値を返せる

    基底クラス(インターフェース)の Eval メソッドの定義を変更することで自由な個数の引数をとる様にしたり、返り値の型を変更することが出来ます。

  3. クラスの内部構造を隠蔽できる

    メソッドにより走査するため、クラス内部の情報を外部に公開する必要がありません。

    上の例で言えば、Num クラスは内部にどのような値を保持しているのかを外部に公開していません。

  4. 木クラスの追加が容易

    上の例に平方根を表すノード Sqrt を追加する場合、SqrtEval メソッドを適切に実装するだけで平方根を含む式木の評価を行えます。

デメリット

  1. 処理が分散する

    上では比較的単純な例を示しましたが、非正規化非均質抽象構文木のような複雑になりやすい木構造の場合、クラスの数が数十以上になってしまうこともありえます。その場合、それぞれのクラスに処理が分散してしまうため、変更などが行いにくくなります。

    また、処理の追加(例: プリティプリント機能 や 型検査機能、最適化機能など)を行うごとに木クラスの定義が膨れあがっていきます。

単純な木構造の走査に向いたパターンと言えます。

2. 外部木訪問器(Visitorパターン)

非均質抽象構文木などの走査によく用いられるパターンです。

ダブルディスパッチにより、訪問した木クラスの型に応じた訪問器のメソッドに処理を振り分けます。

// 外部訪問器インターフェース
interface IVisitor<T> {
    T Visit(Add node);
    T Visit(Mul node);
    T Visit(Num node);
}

// ノード
interface INode {
    // 訪問器の受け入れ
    T Accept<T>(IVisitor<T> visitor);
}

// 加算
class Add: INode {
    public INode Left  { get; }
    public INode Right { get; }
    
    public Add(INode left, INode right) {
        this.Left  = left;
        this.Right = right;
    }
    
    // IVisitor.Visit(Add) を呼び出す
    public T Accept<T>(IVisitor<T> visitor) => visitor.Visit(this);
}

// 乗算
class Mul: INode {
    public INode Left  { get; }
    public INode Right { get; }
    
    public Mul(INode left, INode right) {
        this.Left  = left;
        this.Right = right;
    }
    
    // IVisitor.Visit(Mul) を呼び出す
    public T Accept<T>(IVisitor<T> visitor) => visitor.Visit(this);
}

// 数値
class Num: INode {
    public double Value { get; }
    
    public Num(double value) {
        this.Value = value;
    }
    
    // IVisitor.Visit(Num) を呼び出す
    public T Accept<T>(IVisitor<T> visitor) => visitor.Visit(this);
}

// 評価訪問器
class Evaluator: IVisitor<double> {
    public double Visit(Add node) => node.Left.Accept(this) + node.Right.Accept(this);
    public double Visit(Mul node) => node.Left.Accept(this) * node.Right.Accept(this);
    public double Visit(Num node) => node.Value;
}

class Prog {
    public static void Main() {
        // 1*2 + 3*4
        var tree = new Add(
            new Mul(new Num(1), new Num(2)),
            new Mul(new Num(3), new Num(4))
        );
        
        // 評価器
        var evaluator = new Evaluator();
        
        System.Console.WriteLine(tree.Accept(evaluator));
    }
}

実行結果

14

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

注目して頂きたいのは 各木クラスに定義された Accept メソッドです。

全く同じ定義の関数がそれぞれのクラスで定義されています。これは一見無駄に見えますが、this の型によって呼び出すメソッドを解決するため必要になります。

また、式木の評価は訪問器が担当するため、評価に関する処理が木クラスから切り離されていることがわかります。

メリット

  1. 木構造と処理を分離できる

    処理を訪問器が行うため、木クラスは処理に関する詳細を知る必要がなくなりました。

  2. 訪問器が状態を持てる

    走査メソッドの場合、走査途中の状態は引数として伝播させる必要がありますが、Visitorパターンの場合は訪問器が処理途中の状態を持つことが出来ます。

  3. 機能の追加に強い

    機能を追加する際には、新しく処理を行う訪問器を作成すればよいため、既存のコードを変更する必要がありません。

    例として、式木を文字列に変換する訪問器 Printer の定義を示します。

class Printer: IVisitor<string> {
    public string Visit(Add node) => $"({node.Left.Accept(this)} + {node.Right.Accept(this)})";
    public string Visit(Mul node) => $"({node.Left.Accept(this)} * {node.Right.Accept(this)})";
    public string Visit(Num node) => $"{node.Value}";
}

tree.Accept(new Printer()); // ((1 * 2) + (3 * 4))

デメリット

  1. 木の内部情報を訪問器に公開する必要がある

    上の例であれば、Num クラスの保持している数値に関する情報などの、処理に必要な情報が外部に公開されていることがわかります。

  2. 引数の型や個数が固定

    訪問器によって引数の型や個数を変えることが難しくなります。

    更に、上の例の場合 ジェネリクスを用いて返り値の型を変えていますが、C++の場合は (templateと仮想関数の相性が良くないため) 処理の結果を呼び出し元へと伝播させるためには訪問器の内部状態を利用するなどの工夫が必要になります。

  3. 処理を行うメソッド名が固定

    Interpreterパターンの場合は処理によって適切なメソッド名を付けられますが、上の例の場合は Visit に固定されます。

構文木の解析などの比較的複雑な木構造の走査に向いているパターンと言えます。

3. 外部訪問器(型情報などによる振り分け)

メソッドの振り分けを訪問器の内部で行うパターンです。

// ノード
interface INode {
    // Acceptは必要ない
}

// 加算
class Add: INode {
    // ...
}

// 乗算
class Mul: INode {
    // ...
}

// 数値
class Num: INode {
    // ...
}

// 評価訪問器
class Evaluator {
    public double Dispatch(INode node) {
        // C# 6.0
        if (node is Add) return this.Eval((Add)node);
        if (node is Mul) return this.Eval((Mul)node);
        if (node is Num) return this.Eval((Num)node);
        
        // 処理できなかった
        throw new System.Exception();
    }
    
    private double Eval(Add node) => this.Dispatch(node.Left) + this.Dispatch(node.Right);
    private double Eval(Mul node) => this.Dispatch(node.Left) * this.Dispatch(node.Right);
    private double Eval(Num node) => node.Value;
}

class Prog {
    public static void Main() {
        // 1*2 + 3*4
        var tree = new Add(
            new Mul(new Num(1), new Num(2)),
            new Mul(new Num(3), new Num(4))
        );
        
        // 評価器
        var evaluator = new Evaluator();
        
        System.Console.WriteLine(evaluator.Dispatch(tree));
    }
}

実行結果

14

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

木クラスの Accept メソッドがなくなり、代わりに訪問器の Dispatch メソッドでクラスに応じたメソッドに処理が振り分けられていることがわかります。

mcsが C# 7 に対応していないため if 文を羅列していますが、C# 7 に対応した環境であれば switch 式を用いたパターンマッチングが使える様になるため、よりスマートに記述することが可能になります。

メリット

  1. 処理を行うメソッド名を自由に決められる

  2. 引数の数や個数が自由

  3. 各走査メソッドを private に出来る。

デメリット

  1. 各訪問器に Dispatch メソッドが必要

  2. メソッドの実装忘れがコンパイル時に確認できない

    Dispatch メソッド内のノード型に対応した振り分け機構に実装忘れがあっても、コンパイラはそれを検知できません。実行時に例外が発生してようやく実装忘れを確認出来ます。

このパターンの利点は一見少ないように見えますが、今回の数式微分を行うプログラムでは式木の走査にこれを採用しました。

なぜかと言うと、このパターンとD言語の言語機能である mixin / template mixin の相性が非常に良いためです。

それらの言語機能に関する詳細は省きますが、文法的にクリーンなC言語のマクロを想像すればだいたいそんな感じです。

では、D言語での 評価訪問器 の実装例を以下に示します。

// 訪問器ミックスイン
template Visitor() {
    // 振り分けメソッド
    auto dispatch(string func, Args...)(Node node, Args args) {
        import std.algorithm: castSwitch;
        
        return node.castSwitch!(
            (Add node) => mixin("this." ~ func ~ "(node, args)"),
            (Mul node) => mixin("this." ~ func ~ "(node, args)"),
            (Num node) => mixin("this." ~ func ~ "(node, args)"),
        )();
    }
}

// 評価訪問器
class Evaluator {
    mixin Visitor;
    
    private double eval(Add node) { return this.dispatch!"eval"(node.left) + this.dispatch!"eval"(node.right); }
    private double eval(Mul node) { return this.dispatch!"eval"(node.left) * this.dispatch!"eval"(node.right); }
    private double eval(Num node) { return node.value; }
}

この訪問器の振り分けメソッドは、visitor.dispatch!"eval"(node) のように呼び出します。

上の Visitor が template mixin と呼ばれる言語機能を使った部分で、mixin Visitor; と書かれた部分に Visitor 内部の 宣言 (この場合は dispatch メソッド)を展開します。

これによって複数の訪問器を書く場合にも、dispatch メソッドの定義は一度だけでよく、実装忘れの可能性を低減することができます。

更に、dispatch のテンプレート引数(上の場合は "eval") に適当な文字列を渡せば 振り分け先のメソッドを eval 以外にも変えられる他、node に続いて 引数を渡すことで 複数の引数を振り分け先のメソッドに渡すことができます。

最後に

D言語はそれなりに良い言語なのでみんなも書けばいいと思います。

第二魚雷発射管発射管姫路だいじょうぶじゃないでぃす

この記事は はいふり Advent Calendar 2016 - Adventar の10日目の記事です。

www.adventar.org

間に合いませんでした。

第二魚雷発射管姫路さんが掃海時になぜ助かったのかの話をしたかったのですが間に合いませんでした。

下はお茶を濁すための姫路さんです。

煮たり焼いたりしてください。

f:id:Ryooooooga:20161218103219p:plain

それはそうと

はいふりといえば登場するキャラがみんなかわいいことで有名ですね。

私は第二魚雷発射管の姫路さんがすきです。

魚雷発射管とは

念のため補足しておくと、魚雷発射管は魚雷をしゅぽしゅぽ打ち出すあれです。

晴風には 61cm4連装魚雷発射管 が船体の中央付近に前後 2基 装備されています。*1

それぞれ前側の第一魚雷発射管を りっちゃん(松永 理都子) が、 後ろ側の第二魚雷発射管を かよちゃん こと 姫路 果代子さん が担当していて、 水雷長のメイちゃん(西崎 芽依)の指示で魚雷を発射します。

人物

ちっちゃい。

かわいい。

相撲に詳しい。

あほ毛。

主な登場シーン

魚雷の発射担当という役割上、対比叡戦、対アドミラル・グラフ・シュペー第2戦、対武蔵戦では大活躍だった(はず)のですが画面上にはほとんど登場しませんでした。

(対さるしま戦ではりっちゃんが魚雷(訓練弾)を発射、対シュペー第1戦、対伊201戦では魚雷の残弾が無かったため水雷戦は無し)

また、商店街船「しんばし」の海難救助にはダイバーとして作戦に参加し、被害状況の確認と浸水区画の捜索を担当していました。

そして、姫路さんを語る上で外せないのが機雷掃海作戦です。

機雷掃海作戦

あらまし

機雷の敷設された海域に迷い込んでしまった晴風、安全に後悔航海するために周囲の掃海を行う必要があります。

副長「人力の水中処分は危険だ」
艦長「スキッパーを使おう」
ココ「あれなら小さいので機雷に引っかかる可能性は低いです」
タマ「あん……ぜん*2
艦長「スキッパー乗員には重安全具の装着を」

というわけで晴風の搭載している中型スキッパーを用いた掃海が行われることになりました。

スキッパーの操縦には免許が必要であり、特に中型スキッパー免許の取得には小型スキッパー免許の一年以上の保有が最低条件になります。*3

晴風乗員で中型スキッパー免許を持っているのは艦長、和住 媛萌(ヒメちゃん)ぞな子サトちゃん(勝田 聡子)、そして りっちゃん の4名のみです。*4

そんなこんなで りっちゃん と 姫路さん の二人がスキッパーでの掃海具の曳航を行うことになりました。*5

顛末

結果としてりっちゃんの操縦するスキッパーは係維機雷を支持するケーブルを高々数本*6切断したところで機雷に接触[要出典]し、ふっとばされることと相成りました。

重安全具

さて、はいふり世界のブルマー人はこの現実世界の人間よりも比較的丈夫であることが知られています。

下にその根拠となる幾つかの例を挙げます。

  • 晴風機関科員

    高温の蒸気が噴き出す可能性のある機関室にも関わらず半袖セーラー、あるいは水着で作業。

  • 明石乗員

    半袖で溶接。

  • ヴィルヘルミーナ・ブラウンシュヴァイク・インゲノール・フリーデブルク

    シュペーの15cm砲の至近弾を喰らい、搭乗する小型船は木っ端微塵になるも無傷(ただし数時間気絶)。

  • 艦長 岬明乃

    中型スキッパーで脱落した武蔵の装甲[要出典]に高速で衝突し放り出されるも無傷。

    幼馴染の知名 もえかの危機に動転していた艦長の操縦するスキッパーは、武蔵の航行速度からみても少なくとも60km/hは出ていたと考えられます(なお、中型スキッパーの最大速力は120km/h *7 )。

ということで、頑強なブルマー人(の卵)は基本的に半袖セーラーでなんでもこなしてしまいます。

とはいえ鋼鉄の艦艇を攻撃することを目的とした機雷を相手取るにはいくらなんでもそれだけでは無理があるようで、上の会話の通り重安全具の装着が指示されます。

というわけで以下の画像が重安全具を装備した姫路さん(と りっちゃん)です。

f:id:Ryooooooga:20161218000847j:plain

姫路さん(不安げな表情がかわいい) の手首についているのが恐らくそれです。

本来はこの重安全具の話がしたかったのですが間に合いませんでした。

ともあれ、機雷の至近爆発に巻き込まれた 姫路さんら は重安全具のお陰[要出典]で軽症で済んだのでした。

まとめ

はいふり世界の人間は丈夫。

姫路さんはかわいい。

ハイスクール・フリート ファンブックを買って。

www.hai-furi.com

*1:ハイスクール・フリート ファンブック P110

*2:危険

*3:ハイスクール・フリート ファンブック P114

*4:ハイスクール・フリート ファンブック P114

*5:二人乗りのため 姫路さん は免許がなくても問題はない

*6:画面で確認できるのは少なくとも2本

*7:ハイスクール・フリート ファンブック P114

篤見唯子のスロウスタート

この記事は まんがタイムきらら Advent Calendar 2016 - Adventar の10日目の記事です。

www.adventar.org

はじめに

今年は今までで最も多くの まんがタイムきらら の作品に触れることの出来た一年でした。

その中でも最も私の好きな作品の一つ、篤見唯子先生の「スロウスタート」を紹介したいと思います。

可能な限りネタバレなどは避けます。

作品について

スロウスタート篤見唯子先生 によって まんがタイムきらら 2013年7月号より連載されている作品です。

単行本は3巻まで発売されています。(2016年12月現在)

淡く優しいふんわりとした色使いの特徴的な絵柄がかわいらしいです。

登場人物

※ 単行本内のイラストを貼り付けるのがはばかられたので画像はありません。1巻のAmazonプレビュー、3ページ目あたりを見ながらだとキャラクタの想像がし易いと思います。

主な登場人物をざっくり紹介します。

いち花名はな

主人公。

高校一年生。実家を離れて従姉のおんの管理するアパートで一人暮らしをしている。

人見知りでネガティブ思考、泣き虫で打たれ弱い豆腐メンタル。

みんなには言えない秘密がある。

かわいい。

くら

高校一年生。ヘアピンがトレードマーク。

花名のクラスメイトで人気者、圧倒的コミュ強。天然たらし。

かわいい。

ももたまて

高校一年生。ツインテールの元気っ娘。

栄依子とは同じ中学出身。

重度のオタク。

八重歯。かわいい。

せんごくかむり

高校一年生。ちっちゃくてマイペース。

栄依子とは小学校の同級生。栄依子によく懐いている。

かわいい。

魅力

スロウスタートは言うなれば、花名が自身の抱えた秘密と向き合う物語です。

友人たちと過ごす何気ない日常の中で、時折ふっと鎌首をもたげるそれとどう折り合いをつけるのか。

秘密を打ち明けることで何かが変わってしまうかもしれないという不安と、親しくしてくれる友だちにそれを隠し続けることへの罪悪感。

けれども周囲の優しさの中でゆっくりと成長する花名の姿こそがこの作品の魅了だと思うのです。

かなり短くなってしまいましたが、文字通りいい意味でスロウな作品なのでこれ以上踏み込んだ話をしようとするとどうしてもネタバレを含んでしまうのでこのあたりで。

蛇足ではありますが、あんまり短すぎるのも申し訳ないので拙作の花名とたまてのデフォルメアイコンを投げておきます。酔狂な方が居ましたらTwitterのアイコンなどにご自由にどうぞ。

f:id:Ryooooooga:20161209220312p:plain:w300f:id:Ryooooooga:20161209225600p:plain:w300