小説に登場する若者像を考える
はじめに
いつの時代も若者は、小説の主題となることもある中心的な存在であった。ここでは独断と偏見に基づいて小説をサンプリングして、その時代ごとに描かれている若者について探っていきたいと思う。
戦前
現代は「格差社会」であると言われている。特に経済格差については、ミラノヴィッチによれば、世界的に、現代においてはエリート、すなわち医者や弁護士などの高学歴エリートが、株の配当や不動産所得などを同時に得ており、つまり労働所得が多い者が資本所得が多いという傾向がある。すなわち、資本所得が多い人々は、想像されるような資本家という階層、つまり労働せず、資本だけを持っているような人々というのが大宗を占めるのではなく、実際には所得の多いエリートが投資をすることで資産を形成している、という構図が見えてくるのだ。
特に日本でみられる傾向として、資本所得と労働所得のジニ係数の格差が、他国に比べより顕著であるという図が掲載されている。すなわち、労働所得の格差に比べて、資本所得の格差が大きいことを示唆している。その理由はいくつか考えられるが、一つには預金信仰の強い日本では、そもそも投資をする人が少ないがゆえに、資本所得の有無がそのまま格差に直結していて、これは投資リテラシーと関係している、というような状況があるというのは、雑な推論だろうか。
これは新自由主義的な資本主義(ミラノヴィッチの言葉を借りればリベラル能力資本主義)の帰結として生じているものであるが、社会資本等有形無形の資本は家庭環境によって継承されるという留保はありつつも、お金さえ稼げばその他のことは関係ないという時代になったという意味では、良くも悪くも平等であるとも言える。一方で華族制度が残っている戦前では、文字通り門地による格差というものが歴然と存在していた。
三島由紀夫の「豊饒の海」第一巻「春の雪」において、華族の子息である松枝は、貴族としての所与の生活がある一方で、そうであるがゆえに屈折した感情を持ってしまうに至っている。その発露が恋愛であり、多くの人間を傷つけてしまう結果にはなるが、松枝の友人であり「豊饒の海」ストーリーのキーマンとなる本多が、続編においてだんだん卑屈に、猥雑になっていくのに比べて、純粋な恋愛の帰結として描かれている。
とはいえこの時代の小説で語られる若者は、「坊っちゃん」のような無鉄砲な若者はいるにはいたけれど、依然として「どう生きるか」ということを悩み続けていたように思う。逆に言えば、そういった小説で取り上げられている若者は、それを悩むことができるという性質を持っている点で、戦前のつかの間の平和を享受していた、といえるのかもしれない。実際に、豊饒の海は第二巻、第三巻と進むにつれて、きな臭い戦争に多かれ少なかれ巻き込まれていくことになる。
戦中
戦中の若者は、つまり兵隊であった。古市によれば彼ら若者は老人から期待を受けていたが、それは彼らを単純に兵力、労働力として換算していたからにほかならない。彼らは不幸であった。その不幸は自らの生きる道の選択肢というものがなかったということにある。そう考えると、生きる道を選択できるようになったというのが、戦後以降の人々の生き方を規定することになる。
猪瀬直樹の「昭和16年夏の敗戦」においては(これは小説というよりノンフィクションかもしれないが)、開戦直前に総力戦研究所において日本の若き才英たちが日米開戦のシミュレーションを行い、そして敗北するという結論を導きながら、その意見が黙殺され、日本が開戦に向かって突き進んでいく様子が克明に描き出されている。上層部の決定に振り回されるしかない若者はいつの時代も無力なのだ。
山崎豊子の戦争三部作とよばれる作品群、「不毛地帯」「二つの祖国」「大地の子」では、様々な立場から戦争に翻弄される青年が描かれている。
「不毛地帯」の主人公である壹岐は、戦時中は大本営の参謀本部にいたものの、終戦直後にわたった満州でソ連軍に拘束され、シベリア抑留の憂き目に遭っている。「二つの祖国」は、日系人の兄弟がアメリカと日本のそれぞれで戦争に巻き込まれていく葛藤が描かれている。また「大地の子」では、中国残留孤児の半生が描かれている。いずれの主人公も、戦争を契機に大きく人生が変えられることになる。
どの作品の主人公も、戦時中はその運命に翻弄され、戦争が終わってもその運命から逃れられずに生きなければならないことが示唆されている。こうした物語に留まらず、平成初期にテレビで放送されていた二時間ドラマでも、こうした戦時中の出来事や過ちに、老後になっても囚われる人々の姿が描かれていたように思う。こうしたドラマをみながら、我々が戦後の経済成長と引き替えに忘れてしまっていたものに対する内省があったのではないかと思われる。元号が変わって令和となり、戦争を知る世代が減るにつれて、こうしたモチーフはドラマでも徐々に扱われなくなったが、それによりさらに戦争が我々から遠いものになっているのではなるまいか。
戦後
戦後、日本は進駐軍による日本軍や財閥の解体、農地解放を経て、日本国憲法に基づく民主主義国家として生まれ変わった。その後、朝鮮戦争の特需から経済が回復し、防共防波堤の最前線としての役割が期待されるようになると、軍国主義の看板は資本主義社会における利潤追求にすげ替えられ、後にエコノミックアニマルと揶揄されるような、高度経済成長の時代に邁進していくのである。
戦後の青春小説については、こちらの記事で触れている。
まとめ
強引な解釈、不適切な読み違え、事実と異なる記述については大目に見ていただくとして、若者意識の変遷というものをある程度、各時代の小説の中から読み取ることが可能となっているように感じる。この話題をさらに発展的に捉えていくと、何か見えてくるものがあるのではないだろうか、とまあこのように愚考する次第である。
Konva をハックしたズームレイヤー実装に向けて
Konva は JavaScript 製ライブラリであり、 HTML5 Canvas上に矩形や円、画像などさまざまなオブジェクトを配置し、さらにアニメーションやズームレイヤーなどが整備されているので、ドラッグ・ドロップでオブジェクトを移動させられるなど、インタラクティブな2Dキャンバスを簡単に利用できるようにするライブラリである。
本稿では、Konva 上で画像の拡大・縮小を実装し、更にズームした場合に高精細な画像に徐々に切り替えていく、という「ズームレイヤー」を実装する際に、いかにスムーズに各レイヤーの画像を切り替えるか、という実装を検討したい。具体的に言うと、以下の gif 画像のようなものを実現したいというわけである。
Konva では、 Stage という描画領域にオブジェクトを配置する。Stage には x,y 座標があるのでスクロールを反映できるほか、scale というパラメータがある。scale というパラメータは 1 を等倍として、x軸、y軸それぞれの方向に拡大・縮小をすることができる。例えば、 scale が 2 のときは2倍に画像を拡大していることに相当している。scale が 2 のときに、2倍に拡大したより解像度の高い画像と差し替えることで、ラスタ画像を拡大した際に生じるジャギーなどを緩和して、Google Maps のようなセマンティックズームを実現することができるはずである。
ここで、そのアーキテクチャを実現するための方法をいくつか検討する。まず、以下の4通りの実装方針について考える。以下では、scale=1のときに描画されている画像を元のレイヤーと呼び、拡大していって scale=2 になったときに、2倍になったのでより高画質な画像で書き直すことにしたい。高画質な画像は新しいレイヤーと呼ぶこととする。
- scale が 2 になったときに、改めてそのときの値を scale=1 として再定義し、その後に新しい画像を直接再レンダリングする。
- 利点: 実装が最もシンプルで容易
- 欠点: scale を調節したタイミングで表示されている画像のscaleがリセットされてしまうので、「scaleを調節する ⇒ 現在のzoomLayerの画像でscaleがずれた画像がいったん表示される ⇒ 新しい画像がその上に表示される」となるので、scale がずれた画像が表示される時間が生じてしまう。
- 欠点2: scale がずれた画像が表示されないローディング中に画像がまったく表示されないという問題も残っている。そのため、読み込みが遅い場合は、新しい画像が表示されるまでの間、何も表示されない。
- scale が 2 になったときに、全ての画像を予め全部消してから、改めてそのときの値を scale=1 として再定義し、元のレイヤーの画像を位置を合わせて再描画する。それから、新しいレイヤーの画像を描画する。
- 利点: 新しいレイヤーの画像はスムーズに表示される。
- 欠点: 新しいレイヤーの画像を表示する前に、画面を再描画する必要がある。
- scale が 2 になったときに、その上に新しいレイヤーの画像を描画する。その後、scale=1 として再定義し、新しいレイヤーの画像を描画する。
- 利点: 新しいレイヤーの画像はスムーズに表示される。
- 欠点: 新しいレイヤーの画像が全て表示されたあとで、scale を適切に設定するために、画面を再描画する必要がある。そのため、新しい画像のレイヤーの画像が全て表示されたという状況をフェッチする必要がある。
- scale が変わった時に常にリセットせず、scale が 2倍、4倍、8倍、……のときには新しいレイヤーの画像をサイズを調整してそのまま上に貼り付ける。
- 利点: scale をリセットしないので、画面の再描画なしに拡大・縮小することが可能。
- 欠点: 適切に画像を設定したとしても、画像がscaleに応じて拡大されてしまうので、拡大していくと画像が粗くなり読みにくくなる。テキストオブジェクトも同様に拡大されてしまう。
1の方法ではscaleを変更した際に scale がずれた画像が一瞬表示されてしまうので UX があまりよくない。2~3 の方法ではいずれも、 scale を変える前後で画像を一旦全消去し、それから再描画するという必要がある。そのため、下図のようなちらつきがみられる。
1~3の方法では、scale の値は一定の値域におさまる。これに比べて4の方法は、一番最初に表示していた画像に対する scale という絶対的な値をもとに描画するので、拡大していくと画像がだんだん粗くなってしまう。これは画像を再配置しなおしたとしても、 scale の設定によって勝手に拡大して表示されてしまうので、また配置しているテキストもズームされてしまう。下図が、4 のやり方でうまくいかなかった例である。
結論から言うと、Konva stage の scaling 機能を使っている限り、(a) scaling factor が大きくなりすぎると画像やテキストにジャギーが生じる か (b) ズームレイヤーを切り替えるときに一瞬全ての画像が消える といういずれかの問題から逃れることはできない。(b) の問題を緩和するために、2~3回のズームレイヤーの切り替えの間では 4 の方法を使って画像をスムーズに切り替えるようにして、2~3回ごとに全ての要素を書き換えるようにするという実装案もあるが、根本的な問題解決にはつながらない。
とここまでぐだぐだ書いたところ、そもそも同一のレイヤー上にズームレイヤーの異なる画像を画像を重ねて表示するという実装をしようとしたのがそもそも無理なのであって、ズームレイヤーごとに Konva の layer を配置するようにすればよいという発想に至った。
Konva では、 layer とよばれる、オブジェクトをグループとして扱う機構がある。この layer を、stage 上に複数配置することができる。このとき、ズームレイヤーが切り替わったタイミングで新しい layer を描画することにすれば、画像が読み込まれるのに従って新しい画像が描画されるという仕組みを実装することができる。
注意すべき点として、このレイヤーは、過去のレイヤーを描画時にすぐ削除してしまうと、新しい画像が描画される前に消えてしまう。そのため、拡大前の画像のレイヤーを残しておくと、よりスムーズに拡大することができる。複数のズームレイヤーをまたいだ拡大・縮小が起こることを考えると、レイヤーはたくさん残しておいても良いが、あまりレイヤー数が多くなるとパフォーマンスが落ちるとのワーニングメッセージが表示されるので、ガベコレ的に何世代か前の画像レイヤーを消去する、というような実装にすると良さそうである。
とはいえ、まだこれでもたまに画面のちらつきが見られることがある。結局のところフレームワークを使うというやり方で横着せず、適切なものがあるか不明だが他のライブラリを使うか、あるいは HTML5 Canvas を直接叩く実装をするのが、現時点では妥当そうである。というところで、あまりうまくいかない方法の調査結果となってしまったが、同じ轍を踏まないようにするための備忘録ということで記録しておきたい。
DeepZoom フォーマット (DZI) の解説
Google Maps のような、もともとは1枚絵であるが、拡大すればするほど高精細な画像が表示される、あるいは拡大するたびに、情報の密度が異なる画像が表示されるようなアーキテクチャを Web 上で実装したいと思う。
こうしたアーキテクチャを実装するにあたっては、以下の2つの要素が必要になる。
- バックエンドでズームレベルに応じて異なる解像度の画像を提供する
- フロントエンド側では、拡大縮小やスクロールといった操作に応じて、各レイヤーの画像を適切に配置する
フロントエンド側では、オープンソースで OpenSeadragon とよばれる実装が入手できる中でもクオリティの高い実装の1つであると考えられる。OpenSeadragon は複数の、フロントエンドとバックエンドの間のインターフェースとなるフォーマットをサポートしている。
- Legacy Image Pyramids
- IIIF (International Image Interoperability Framework)
- DZI (Deep Zoom Images)
- OSM (Open Street Maps)
- TMS (Tiled Map Service)
- Zoomify
- Custom Tile Sources
- Simple Image
そのなかで本稿では、DZI フォーマットに焦点をあてて解説する。DZI フォーマットはフォーマット内の規約によって様々なことが決まっていてカスタマイズ性が少ない反面、少ない実装量で拡大・縮小可能な画像を生成することができる。
DZI フォーマットは今は無き Microsoft Silverlight で利用されているフォーマットである。DZI フォーマットを利用すると、一枚の巨大な画像を、部分画像に分割することで、拡大・縮小した際に切れ目無く、解像度を保ったまま表示することができる。フォーマット自体は更新されていないが、少なくともフォーマット自体の完成度は十分なので、このフォーマットを利用したところで生じる問題は少ないと考えられる。本稿では、DZI フォーマットに従った画像群を作る上で気をつけなければならないことをまとめる。
DZI
DeepZoomフォーマットは、基本的にはユーザー側で設定する内容が極めて少なく、等倍の画像サイズと、各ズームレイヤーにおける、1タイルあたりのpixel数を予め指定するだけである。各ズームレイヤーにおいて、どの位置の画像を配置すべきかについては、フォーマットの規約に基づいて自動的に計算される。そのため、ユーザーは設定ファイルと、各ズームレイヤーの画像を配置するだけで、拡大・縮小可能な画像を提供することができる。
例えば、これらのツールのうちどれかを使って階層的な画像を作ることができる。
ここでは、これらのツールが具体的にどのような画像を配置しているのかを記述する。
まず、画像を配置するルートディレクトリに、 .dzi
拡張子で以下のようなJSON ファイルを置く必要がある。
{ "Image": { "xmlns": "http://schemas.microsoft.com/deepzoom/2008", "Format": "jpg", "Overlap": "0", "TileSize": "256", "Size": { "Height": "65536", "Width": "65536" } } }
Overlap の値を0以外に指定すると、画像と画像の間にそのpixel数だけオーバーラップした画像を容易することができる。これは、例えば画像の切れ目に文字が重なるような場合に、オーバーラップした画像を予め作っておくといったことができるが、ここではOverlap=0とした場合で議論を進めている。
たとえば、上記の場合、すなわちTileSize=256, Width=65536, Height=65536の場合を考える。この場合では、もとの全体画像が等倍の場合に65536px x 65536px であったという計算になる。このときに、 ズームレイヤーの個数はどのように決定されるかというと
この計算式で決まる。この場合では、どのようなタイルが生成されるのかを、以下のコードを用いてシミュレートする。(元のコードは https://www.gasi.ch/blog/inside-deep-zoom-2 にあったものを移植した。)
def getMaximumLevel(width, height) return (Math.log2([width, height].max)).ceil end def computeLevels( width, height, tileSize) maxLevel= getMaximumLevel( width, height ) maxLevel.downto(0) do |level| columns = ( width * 1.0 / tileSize ).ceil rows = ( height * 1.0 / tileSize ).ceil puts "level #{level} is #{width} x #{height} ( #{columns} columns, #{rows} rows )" width = ( width / 2.0 ).ceil height = ( height / 2.0 ).ceil end end computeLevels(65536, 65536, 256)
これを実行した結果は以下である。
level 16 is 65536 x 65536 ( 256 columns, 256 rows ) level 15 is 32768 x 32768 ( 128 columns, 128 rows ) level 14 is 16384 x 16384 ( 64 columns, 64 rows ) level 13 is 8192 x 8192 ( 32 columns, 32 rows ) level 12 is 4096 x 4096 ( 16 columns, 16 rows ) level 11 is 2048 x 2048 ( 8 columns, 8 rows ) level 10 is 1024 x 1024 ( 4 columns, 4 rows ) level 9 is 512 x 512 ( 2 columns, 2 rows ) level 8 is 256 x 256 ( 1 columns, 1 rows ) level 7 is 128 x 128 ( 1 columns, 1 rows ) level 6 is 64 x 64 ( 1 columns, 1 rows ) level 5 is 32 x 32 ( 1 columns, 1 rows ) level 4 is 16 x 16 ( 1 columns, 1 rows ) level 3 is 8 x 8 ( 1 columns, 1 rows ) level 2 is 4 x 4 ( 1 columns, 1 rows ) level 1 is 2 x 2 ( 1 columns, 1 rows ) level 0 is 1 x 1 ( 1 columns, 1 rows )
この結果を解釈すると、
- ZoomLevel=0 のときに、1x1 の1タイルの画像(全体のサムネイル)だけが存在し、
- ZoomLevel=1 のときに、2x2のタイルが1枚存在する。
- .....
- ZoomLevel=7 のときに、128*128のタイルが1枚存在する。
- ZoomLevel=8のときに、256256のタイルが1枚存在する。これ以降は、画像サイズは全て256256となる。
- ZoomLevel=9 のときに、256*256のタイルが4枚存在する。
- ……
- ZoomLevel=16 (等倍ズーム)のときに、256x256 のタイルが256*256枚並べられる。単純には65,536枚の画像が必要となる。
そのため、ここで注意しなければならないのは、
- ZoomLevel が 一定以下のサイズになると、最小単位よりも小さいタイル1枚によって構成されることになる。だいたいの場合で画像を縮小しすぎになってしまうので、これらのZoomLevelは使われないことが多い。
- ZoomLevel がある程度大きくなると、解像度が大きくなって表示したい画像サイズがタイルサイズとして指定してあるものを上回る。すると、
次に、横長になるような場合を考える。例えば65536 x 256 の場合だと、以下のようになる。
level 16 is 65536 x 256 ( 256 columns, 1 rows ) level 15 is 32768 x 128 ( 128 columns, 1 rows ) level 14 is 16384 x 64 ( 64 columns, 1 rows ) level 13 is 8192 x 32 ( 32 columns, 1 rows ) level 12 is 4096 x 16 ( 16 columns, 1 rows ) level 11 is 2048 x 8 ( 8 columns, 1 rows ) level 10 is 1024 x 4 ( 4 columns, 1 rows ) level 9 is 512 x 2 ( 2 columns, 1 rows ) level 8 is 256 x 1 ( 1 columns, 1 rows ) level 7 is 128 x 1 ( 1 columns, 1 rows ) level 6 is 64 x 1 ( 1 columns, 1 rows ) level 5 is 32 x 1 ( 1 columns, 1 rows ) level 4 is 16 x 1 ( 1 columns, 1 rows ) level 3 is 8 x 1 ( 1 columns, 1 rows ) level 2 is 4 x 1 ( 1 columns, 1 rows ) level 1 is 2 x 1 ( 1 columns, 1 rows ) level 0 is 1 x 1 ( 1 columns, 1 rows )
- ZoomLevel=0 のときに、1x1 の1タイルの画像(全体のサムネイル)だけが存在し、
- ZoomLevel=1 のときに、2x1のタイルが1枚存在する。
- .....
- ZoomLevel=7 のときに、128*1のタイルが1枚存在する。
- ZoomLevel=8のときに、256*1のタイルが1枚存在する。
- ZoomLevel=9 のときに、256*2のタイルが2枚存在する。
- ……
- ZoomLevel=16 (等倍ズーム)のときに、256x256 のタイルが256枚並べられる。単純には256枚の画像が必要となる。
横長になる場合では、以下の点に留意しなければならない。
- 最下層のレイヤーのみは、256*256 の等幅タイルを配置することになるが、それより上位レイヤーでは、画像サイズが
- 低いレイヤーの画像が、横長の小さい画像になるので、もとの画像が等幅の場合に比べて
画像をどう配置するか?
画像はズームレイヤー・横index・縦indexの3つの値によって example_files/<zoom_level>/<row>_<column>.jpg
というように指定される。例えば、example_files/0/0_0.jpg
である。基本的には、各ズームレイヤー間は縦横が2倍ずつの差になっているので、例えば横長の画像の場合は、 example_files/12/25_0.jpg
の画像を拡大した場合は example_files/13/50_0.jpg
と example_files/13/51_0.jpg
になるし、縮小した場合は example_files/11/12_0.png
の一部に相当する。(縦横幅が同じであれば、上位レイヤーの1枚が下位レイヤーの4枚に対応する。)
4096x4096 tileSize=256の場合の画像の例は以下である。
- layer=8
- layer=7
上記のように、レイヤー間のタイルの長さは2倍ずつ差がるので、面積としては4倍の差になる。
* deepzoom.dzi * 0/ * 0_0.jpg * 1/ * 0_0.jpg * 2/ * 0_0.jpg * ...
全体として、画像は上記のようなディレクトリ構造で配置される。画像のサイズがタイルサイズの倍数で割りきれない場合は、右端ないし下端の画像サイズが短くなることによって適宜調節される。
OpenSeadragon 側ではどのような設定が必要となるか?
<div id="openseadragon1" style="width: 95%; height: 95%;"></div> <script src="openseadragon.min.js"></script> <script type="text/javascript"> OpenSeadragon.DEFAULT_SETTINGS.timeout = 600000; var viewer = OpenSeadragon({ id: "openseadragon1", panVertical: true, prefixUrl: "/example_files/", navigatorSizeRatio: 0.25, wrapHorizontal: false, debugMode: false, showNavigator: true, navigatorHeight: "10px", navigatorWidth: "845px", defaultZoomLevel: 26, minZoomLevel: 10, visibilityRatio: 0.5, tileSources: "/deepzoom.dzi", }); </script>
OpenSeadragon を表示したいHTMLファイルから、 openseadragon.min.js
を呼び出す。パラメータについて、以下の設定を変えると便利な場合がある。
- 画像が重くてロードに時間がかかり、タイムアウトしてしまう場合は、
OpenSeadragon.DEFAULT_SETTINGS.timeout
の値を大きくする。 - ZoomLevelの値が小さい場合は、画像サイズが小さすぎて見られないのであまり利用されない。そこで、minZoomLevel を大きめの値に設定すると、細かい画像をロードしにいかなくて済む。これは先述の場合に対応した工夫である。
- visibilityRatio は、minZoomLevel まで縮小したとき、それ以上に縮小することを許容するかどうかを指定する。visibilityRatio=1.0 のときは、minZoomLevel の画像はそれ以上縮小されない。例えば visibilityRatio=0.5 にすると、半分のサイズになるまでは縮小される。
フロントエンドではどのような実装が考えられるか?
現状でオープンソースな実装を使おうとすると、OpenSeadragon が最有力候補であると考えられる。このライブラリはなかなか完成度が高いが、以下のような難点がある。
- 1枚の画像が 10MB を越えると、なぜか表示されなくなる。そのため、1タイルあたりの画像が大きい場合に、高精細な画像を利用するのが難しくなるケースが存在する。
- 拡大/縮小の方向が縦横の比率を保った方向に限定されており、例えば横方向のみの拡大/縮小などがサポートされていない。そのため、例えば年表のような画像を作る場合では、縦方向に画像が拡大されてしまって不都合になってしまう。
- 一度キャッシュされた画像は書き換わらない。そのため、バックエンド側で動的に画像が置き換わるような仕組みを実装するのが難しい。
ところで、このalternative な実装を作ろうとすると(実際に実装しようと試みたのだが)、 (1) OpenSeadragon はスムーズに画像を切り替えられるように丁寧に実装されている、 (2) パフォーマンスを保つために、ロントエンド上に画像をキャッシュする実装が必要、(3) OpenSeadragon はズームを補間している というところで、なかなか適切に実装するのは大変だと考えられる。
参考ページ
https://openseadragon.github.io/examples/tilesource-dzi/
https://github.com/openseadragon/openseadragon/wiki/The-DZI-File-Format
Rust のポリモルフィズムとトレイトオブジェクト
Rust で、「ある構造体のフィールドに、ある条件(=トレイト)を満たす他の構造体と動的に取り替え可能なフィールドを設定したい」と思うことがある。例えば、struct Binary で定義している、バイナリファイルを、struct BinaryReader 経由で読み込むときに、そのデータがstruct AであるかBであるかが、読み込むまで分からないとする。このAとBは、どちらもMyTraitを実装しているとする。この状況に対して、どう実装すべきかについて調査したことをまとめる。
例えば、以下のようなトレイトと型を定義したいhttps://doc.rust-lang.org/nomicon/exotic-sizes.htmlとする。
trait MyTrait pub struct MyStruct { my_trait: MyTrait }
しかし、こういう構造体のインスタンスを作る際に、 doesn't have a size known at compile-time といって怒られてしまう。また特に、
trait MyTrait { fn to_stream<W: io::Write>(&self, stream: &mut W) -> io::Result<()>; fn from_stream<R: io::Read>(&mut self, stream: &mut R) -> io::Result<bool>; }
さらに、MyTrait の定義を上記のように拡張すると、Test2 トレイトの内部の関数にもジェネリックがあるので、 trait cannot be made into an object と言われる。そこで、そのようなトレイトをフィールドに持つような構造体を定義する方法について、以下のように調査した。
ジェネリクス(静的な方法)
このとき、単純にトレイトそれ自体を型の定義に入れてしまうと、そのインスタンスに対して、コンパイル時に型を決める必要がある。例えば
trait MyTrait { fn new() -> Self; } pub struct MyStruct<T: MyTrait> { my_trait: T } impl<T: MyTrait> MyStruct<T> { pub fn new() -> C<T> { C{data: T::new()} } } fn main() { let struct = MyStruct::new(); }
これはコンパイルが通らない。なぜかというと、MyStruct の型が結局決まらないからだ。これがビルドできるようにするには、
struct A { data: Vec<u32> } impl MyTrait for A { fn new() -> A { return A{data: vec![]} } } fn main() { let struct: MyStruct<A> = MyStruct::new(); }
上記のように、コンパイル時に型を決定しなくてはならない。しかしある場合では、この型の決め方を動的にしたいという場合があるだろう。というところで、以下の方法を検討する。
dyn Trait
として持つ場合(動的サイズ型)
Rust では可変スタックの使用を許可しないため、基本的にトレイトオブジェクトをスタックに格納することができない。そのため、以下のようにする。
pub struct MyStruct<'a> { my_trait: &'a dyn MyTrait, } pub struct MyStruct { my_trait: Box<dyn MyTrait>, }
対応関係としては、Box<Trait>
はスマートポインタ、 &Trait
は参照型である。Box<dyn Trait>
とすることで、その struct の定義に、トレイトが登場しなくなる。そのため、このMyStruct のインスタンスを作る際には、具体的に内部のフィールドがどの型になるかがコンパイル時に決まっている必要はない。この方式では、Box を使うので、実体はヒープに格納されることになる。Rust では、Box や Arc、 Rc にトレイトを格納すれば、構造体にはそれへのポインタを持つだけでよいためだ。必ず、1回しか参照されない場合は Box<>
で領域を確保すれば良い。あるオブジェクトをスマートポイントによって、参照カウントによって共有する場合に、 Arc/Rc を使う。Rc はスレッド安全ではないが、Arc はスレッド安全という違いがある。
こうしたやり方は、 late-binding とも呼ばれる。しかしながらそうしてしまうと、2つほど懸念がある。1つ目は、静的に解決するのに比べて、vtable を利用してトレイトにアクセスするので、単なるポインタに比べて若干遅くなるということだ。データアドレスとvtableアドレスの両方をエンコードするファットポインタとなっている。2つ目は、「そもそもそこに格納されている型が何なのか?」の情報が失われてしまうので、別立てで持っておかないといけない。この問題について具体的に言及しているのが、以下のブログ記事だ。
.unwrap() .as_ref() .as_any() .downcast_ref::<A>() .unwrap();
このように、元の型(A)に戻すために、長いメソッドチェーンを記述する必要があり、きれいではない。
Enum Wrapper pattern
これに対して、Enum で元の型を対応するパターンマッチングを実装することを、先述のブログ記事の著者は推奨している。Rust での列挙型はタグ付きユニオンであるため、列挙型の最大のエントリ程度メモリフットプリント(およびタイプ情報用にもう少し)が必要で、サイズはコンパイル時に既知であるため、ヒープは使わなくてすむ。
enum Format { A(A), B(B), C(C), } pub struct MyStruct { my_trait: Format }
もちろんこの方法を使うと、至る所でパターンマッチを用いて、「この型がどの型だったら」という条件文を記述する必要がある。つまりメモリの消費量が多くなり、コードがわずかに多くなる。
参考
Rust で Write Offset を取得したい
tl;dr
Rust で、Write + Seek トレイトを実装したストリームにバイナリを書き出していくときに、いまどれだけ書き出したのかが分かっているとする。このとき 書き始めた位置の Offset を持っておくと、その offset 位置から読み出したり、逆にその位置からデータを上書きして記録するために、ランダムアクセスすることができるようになるということで、利点がある。ここでは、そのやり方を検討する。
例えば、好きな位置からWrite するのは、このライブラリが利用可能である。
https://docs.rs/positioned-io/0.2.2/positioned_io/
では、どの位置からWrite すべきかを、書き出すときに index として持っておくには、どうしたら良いだろうか。
まず現在は、バイナリ列を出力するために、byteorder というLittleEndian/BigEndian を切り替えることができる library を利用して整数を出力しているほか、文字列はu8 のベクトルとして、 std::io の write_all で出力しているとする。このとき、write offset を記録する方法として、調べた限り、2つの方法が有力であるようにみえる。
- 今書き出した位置と、 Seek:0 からの距離を計算する。
- Write trait を継承した struct を作り、それは write の戻り値を返すことで、何byte 書き込んだかを保持しておくことができる。
それぞれの方法について、概説する。
1. 今書き出した位置と、 Seek:0 からの距離を毎回取得するようにする
Write + Seek trait を実装したWriter に書き出していくとき、そのたびに今書き出した位置と、seek:0 からの距離を計算すると、それが今書き込んだ領域の逆算となる。
https://doc.rust-lang.org/std/io/trait.Seek.html
let new_position = file.seek(SeekFrom::Start(any_offset)).unwrap(); println!("{:?}", new_position);
これの派生版として、 Cursor
を用いるケースもある。これは、上述のコードの代わりに以下のように取得できる。
let mut cursor = Cursor::new(contents); cursor.seek(SeekFrom::Start(any_offset)); println!("{:?}", cursor.position());
うまく工夫すると,毎回先頭から計算するのではなく、前回書き込んだ位置からの差分として計算することも可能になるはずである。
2. Write trait を継承した struct を作り、それは write の戻り値を返すことで、何byte 書き込んだかを保持しておくことができる。
この方法では、 write の戻り値として、何byte書き込んだかというのを返してくれるので、それをカウンタに保持しておくことで、どのぐらい読んだかを知ることができる。この方法を実装したのが以下のライブラリである。
https://docs.rs/crate/count-write/0.1.0/source/src/lib.rs
ただしこの方法の欠点として、write_all
や write_fmt
のような,戻り値でデータを書き込んだ長さを返さないようなデータ構造においては、その長さを取得できないという問題がある。
これ以外に、有用な方法はあるだろうか。
参考
https://stackoverflow.com/questions/34878970/how-to-get-current-cursor-position-in-file
Python から Rust を呼ぶライブラリを比べる
tl;dr
Rust で実装されたライブラリに対して Python 側から関数呼び出しをしたいと考えたとき、Rust に対して Python binding を提供してくれるライブラリとして maturin、setuptools-rust、milksnake があり、それぞれのツールの間で微妙に特徴が違うので、その試行錯誤の過程を紹介する。
基本的な流れ
- ユーザー自身の環境で、dylib を生成するオプションで rust をコンパイルし、 共有ライブラリを作る。
- python 側から、 FFI を介して共有ライブラリに対して関数呼び出しを行う。
- 上記を、pip install を叩いたときにワンストップでインストールできるような状態にしておく。
上記のライブラリでは、Python の構造体やクラスなどを、Rust から透過的に扱うことのできるようなインターフェースを提供している。Python のある一部分のコードを高速化するために Rust を呼び出すというケースを想定していると思われるが、今回はより一般的に、Rust で実装したライブラリを pub extern "C"
したコードを、 Python 側から呼び出すということを想定している。上記のコードを1つ1つ実装してもそれほど手間ではないかもしれないが、先人が踏んだ落とし穴をできるだけすり抜けたいと思うと、ライブラリを利用するのが最もシンプルなアイディアだろう。
結論から述べておくと、maturin と setuptools-rs は、python3 の環境があることが前提となっている。
maturin
いわゆるzero-conf 系のツールであり、必要最小限の定義だけすると、あとはよしなに調節してビルドしてくれるというツールである。python 3.5+対応。上記のスクリプトが、 wheel を自動で生成してくれるので、これを使ってみることにする。
$ pip3 install maturin $ maturin build
これをmacos で実行すると、 target/wheels/<script 名>-0.1.0-py2.py3-none-macosx_10_8_x86_64.whl
が作られる。ただし、 wheel のビルドは環境依存なので、単純にそのwheel をlinux 環境に持って行っても動かない。そこで、一つの方法としては、様々な環境で動作する wheel をそれぞれ作るという方針が考えられる。
$ docker pull konstin2/maturin:master $ docker run --rm -v $(pwd):/io konstin2/maturin build
上記のコマンドを叩けば、一応あらゆる環境で動作可能なバイナリが作れるらしい。
そうでない場合は、ユーザーの手元でビルドしてもらうようにする必要がある。つまりwheel を pre-built しておくのではなく、相手の環境でビルドできるような状態でソースコードを頒布したいと考えた時、 pyproject.toml
に記述するという方法がある。そうすると、 pip install .
コマンドで、python library としてインストールすることができるようになる。なお、伝統的にpythonにおいてライブラリの設定は setup.py
が利用されており、pipのバージョンがあまりにも古い場合は pyproject.toml
が動作しないが、ユーザがpip コマンドは最新のものを入れているだろうという想定で、これでいくことにする。
[build-system] requires = ["maturin"] build-backend = "maturin" [tool.maturin] bindings = "cffi" manylinux = "off"
公式サイトによれば、bindings は、 cffi のほか、pyo3, rust-cpythonなどを利用できるとなっている。 manylinux を off にしないと、以下のようなエラーメッセージがでて終了することがある。
💥 maturin failed Caused by: Failed to ensure manylinux compliance Caused by: Your library is not manylinux compliant because it links the following forbidden libraries: ["libz.so.1"]
maturin は、外部ライブラリにリンクしている状況を検知して、 multilinux ビルドできないことを検知しているようだ。各ユーザーがpip install で入れる場合には、multilinux 対応する必要が無いので、その機能はoffにすると、とりあえずビルドできるようになった。これで、ユーザーのlinux/mac/windows 環境でもビルドしてそれぞれでインストールしてもらえるようになった。
ところで、もう少し凝ったことをする必要がある場合は、 setup.py
に相当するものを自分で書けた方が良い*1。そこで、次の2つのツールも試す。
setuptools-rust
これはSetuptools pluginである。 setuptools-rust/html-py-ever at master · PyO3/setuptools-rust · GitHub の例にあるように、setup.py, MANIFEST.in, pyproject.toml, build-wheels.sh を記述する必要がある。build-wheels.sh の中に、cp27 という文字列があるので、一見すると python2 の環境でも動きそうだが、実際にはpython2 しかない環境でビルドしようとする場合、以下のエラーが出た。
Running `rustc --crate-name build_script_build --edition=2018 ~/.cargo/registry/src/github.com-1ecc6299db9ec823/pyo3-0.8.5/build.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type bin --emit=dep-info,link -C opt-level=3 --cfg 'feature="default"' --cfg 'feature="extension-module"' --cfg 'feature="python3"' -C metadata=c13070d14f8e5f9c -C extra-filename=-c13070d14f8e5f9c --out-dir /tmp/pip-req-build-sf23o2hn/target/release/build/pyo3-c13070d14f8e5f9c -L dependency=/tmp/pip-req-build-sf23o2hn/target/release/deps --extern lazy_static=/tmp/pip-req-build-sf23o2hn/target/release/deps/liblazy_static-1cc38642f95bd1c1.rlib --extern regex=/tmp/pip-req-build-sf23o2hn/target/release/deps/libregex-e4869bcab7e72b4a.rlib --extern serde=/tmp/pip-req-build-sf23o2hn/target/release/deps/libserde-198eb2bbcb756b12.rlib --extern serde_json=/tmp/pip-req-build-sf23o2hn/target/release/deps/libserde_json-7bf7d5b71c87fe3c.rlib --extern version_check=/tmp/pip-req-build-sf23o2hn/target/release/deps/libversion_check-65476cb62a0489dd.rlib --cap-lints allow` Running `/tmp/pip-req-build-sf23o2hn/target/release/build/pyo3-c13070d14f8e5f9c/build-script-build` error: failed to run custom build command for `pyo3 v0.8.5` Caused by: process didn't exit successfully: `/tmp/pip-req-build-sf23o2hn/target/release/build/pyo3-c13070d14f8e5f9c/build-script-build` (exit code: 101) --- stderr thread 'main' panicked at 'Python 3.x interpreter not found', ~/.cargo/registry/src/github.com-1ecc6299db9ec823/pyo3-0.8.5/build.rs:28:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
python3 interpreter がないといってエラーが出ている。setuptools-rust は pyo3 というライブラリに依存していて、 pyo3 はpython3 のinterpreter を要求しているようにみえる。上記で見てきたように、pyo3 organization 以下のライブラリはどれもpython3 しか対応していない。いかに python2 がoutdated であるとはいえ、少しの改変でpython2 も対応しているほうが望ましいという立場から、silksnake も試してみることにする。
もし、各環境に応じたwheel を作れば良いという立場であれば、 docker image を使ってビルドすれば、特に問題は無い。
milksnake
python2, python3 のどちらの環境でもビルドを通るようにしたい場合は、このライブラリを使うのが一番簡単である。といってもやっていることはそれほど複雑ではなく、 setup.py
にフックして cargo build --release
を実行するというのがポイントである。こちらも、setuptools-rust と同様に記述しなければならないスクリプトは多いが、特に落とし穴は少ない。
python からは、以下のようにコードを呼び出すことができる。
from example import lib print(lib) print(lib.hello_rust()) print(lib.test())
ビルド時に cbindgen
というライブラリがC のヘッダファイルを出力し、そのヘッダファイルをもとに、cffi を介してビルドされた共有ライブラリにアクセスして、関数呼び出しを行うという仕組みになっていると考えられる。milksnake は最近活発に更新されていないというのが心配だが、その点を除いては問題無く動作した。
他の言語へのバインディング
ここはメモ程度に、検索した結果を貼っておく。
neon: JavaScript でつなげる場合。ただしこのライブラリでは、 JxResult という JavaScript に返す型を定義しなければならないので、Rust にも手を加える必要があってつらいようにみえる。
https://github.com/neon-bindings/neon
https://github.com/neon-bindings/examples
fn make_an_array(mut cx: FunctionContext) -> JsResult<JsArray> { // Create some values: let n = cx.number(9000); let s = cx.string("hello"); let b = cx.boolean(true); // Create a new array: let array: Handle<JsArray> = cx.empty_array(); // Push the values into the array: array.set(&mut cx, 0, n)?; array.set(&mut cx, 1, s)?; array.set(&mut cx, 2, b)?; // Return the array: Ok(array) } register_module!(mut cx, { cx.export_function("makeAnArray", make_an_array) })
*1:例えば、cargo が入っていない環境ではrustupで自動的にrust をインストールしてほしいという場合など
Docker / Singularity の実用面での違い
Singularity と Docker の互換性の問題
Singularity は、 Docker image を変換してSingularity image として実行することができる機能を有している。Singularity が導入されているマシンでは、no-root でDocker image を実行することができる。このほかにも、Singularity は、明示的にマウントをしなくとも、カレントディレクトリが、Singularity コンテナ内のワークディレクトリとなるように、Docker に比べてファイルシステムの isolation 制約が一部緩和されているなど、特定の用途にとっては便利なことが多い。しかしながら、Singularity と Docker を同一視して扱ってしまうと色々な落とし穴にはまる。ここでは、そのいくつかを紹介したい。
Docker run vs Singularity run
まず、docker run と singularity run は異なるインターフェースになってるので注意。例えば、dockerhub 上にある同一のイメージに対して、同じ引数でを実行する際でも、以下のようになる。
- Docker 上で、 DockerfileでEntrypoint に指定しているコマンドに対して引数を与えて実行したいとき
$ docker run --rm <image_name> 1 2 3 4
- Singularity 上で、 DockerfileでEntrypoint に指定しているコマンドに対して引数を与えて実行したいとき
$ singularity -s run docker://<image_name_on_docker_hub> 1 2 3 4
- Docker 上で、 DockerfileでEntrypoint に指定しているコマンドを無視して別のコマンドを実行したいとき
$ docker run --entrypoint <another_command> --rm <image_name> 1 2 3 4
- Singularity 上で、DockerfileでEntrypoint に指定しているコマンドを無視して別のコマンドを実行したいとき
$ singularity -s exec docker://<image_name_on_docker_hub> <another_command> 1 2 3 4
このように、実用的な面からは、 Singularity では、run
と exec
の間でコマンドの役割が分離しているとみることができるだろう。
次に、Dockerfile で latest と指定していても、あるいはdocker run で latest と指定していても、 dockerhub を毎回みにいっているわけではなく、あくまでローカルにある image の最新の docker image を利用しているに過ぎないので、dockerhub 上の更新が反映されない。これに比べて、Singularity は、 latest とタグを指定していると毎回 dockerhub に問い合わせに行くので、最新の docker image をもとに singularity image が構築される。
Docker image をそのままSingularity に持っていくと、image によってはエラーを出力することがある。例えば、neo4j のdocker を動かそうとすると、
$ singularity -s run docker://neo4j:latest [WARN tini (15358)] Tini is not running as PID 1 and isn't registered as a child subreaper. Zombie processes will not be re-parented to Tini, so zombie reaping won't work. To fix the problem, use the -s option or set the environment variable TINI_SUBREAPER to register Tini as a child subreaper, or run Tini as PID 1. /docker-entrypoint.sh: line 3: $1: unbound variable
上記のようなエラーが出る。個人的な印象だが、Singiularity で緩められた isolation の制約のために、isolation されていることを前提に構築された Docker image はそのままでは動かないようにみえる。これは、こういうDocker container はエラーを吐くというように、原因を一意に特定するのは難しく、試しに動作させてみてうまくいかなかったらやり直す、ということを繰り返すしてデバッグすることになりがちである。
また、Entrypoint で動作する、Docker 内のスクリプトを記述するときも、 Singularity であれば pwd
がマウントされているという想定だが、 Docker であればそうではないので、両方に対応したスクリプトを書くときに、どのディレクトリに自分が動かしたいデータが入っているかに留意して記述する必要がある。例えば、Docker image は WORKDIR には元から置いてあるデータが入っていて、マウントするディレクトリはWORKDIR とは別になるときに、Singularity ではWORKDIR がマウントされてしまっているので、WORKDIR に元から入っているファイルは使えなくなることがある、など。
Miscellaneous
Dockerfile を書くときに気になっていること
以下は未解決の問題である。
- build するときに、任意の途中の step の intermediate container をもとに再実行する方法はないのだろうか?
- どのbuild image を使えば良いのか分からない。公式がサポートしているようなライブラリでは、slim 版と alpine 版もしばしば提供されているが、どのbuild image を使うと良いか分からない。
実際には、そのライブラリ内部で依存しているライブラリやパッケージに基づいて、インストールがしやすいものを選ぶ、ということになりがちな気がする。片方から片方に移行しようとする際には、Debian と Alpine では、公式リポジトリから提供されているパッケージ名が異なることがあることから注意が必要である。
Docker で、Docker image の保存領域を変更しようと思った際に叩くコマンド
/app2# grep ExecStart /lib/systemd/system/docker.service ExecStart=/usr/bin/dockerd -H fd:// -g /app2/docker
three.jsでテキストの向きをカメラに追従させる
Three.js で三次元の可視化を実装するときに、カメラ移動を実装して、様々な角度からその三次元データを見られるようにしたいと考えることがある。このときに必要なマウスドラッグやマウスホイールによるカメラ移動は、OrbitControls.js
を使うと苦も無く実装できる。
このとき、Three.js で三次元空間上にテキストを配置してあったとする。その状態で視点を変えると、デフォルトの設定ではテキストの向きが配置時の向きのまま固定されてしまっているので、テキストに対して垂直方向に視点を変えると、そのテキストが読めなくなってしまう。
そのため、カメラの位置に追従し、テキストが常にユーザー側に向いている実装に変更したいと考えた。このとき、試したことや調べたことをまとめる。
うまくいった方法
↑のような実装をしたい時の検索ワードは、face camera
, face user
, look at camera
などのキーワードを用いると良いというのが、調べながら分かってきた。
text.quaternion.copy( camera.quaternion );
オブジェクトの向きは、three.jsなどの各種3Dツールでは、クォータニオンによって定義されている。この↑のコードをオブジェクトの定義時ではなくて、render
時に書くことで、カメラの回転角と、回転させたいオブジェクトの回転角を同期させることができるようになった。
試してうまくいかなかったこと
検索して最初に出てくる方法は、lookAt
を使う方法である。これは、 OrbitControls.js
を使ったときは利用できないので、うまくいかない。
text.lookAt( camera );
また奥の手として、テキストを画像として貼り付けてしまえば、カメラの向きに画像の向きが固定されるということを利用して、いったんテキストをSVGに吐いてから*1、それをSpriteとして貼り付けるということも考えたが、これもうまく動かなかった。
$ gem install text2svg
var map = THREE.ImageUtils.loadTexture( "fonts/text.svg" ); var material = new THREE.SpriteMaterial( { map: map, color: 0xffffff, fog: true } ); var titleX = new THREE.Sprite( material ); titleX.position.x = xScale(vpts.xMin) - 6, titleX.position.y = 2; scatterPlot.add( titleX ); var geometry2 = new THREE.Geometry(); var vertex2 = new THREE.Vector3(xScale(vpts.xMin) - 6, 2, 0); geometry2.vertices.push( vertex2 ); var material2 = new THREE.PointCloudMaterial( { size: 100, sizeAttenuation: false, map: map, alphaTest: 0.5, transparent: true } ); var titleX = new THREE.PointCloud( geometry2, material2 ); scatterPlot.add( titleX );
いまのtextはTHREE.Textureで作っていたが、THREE.Spriteで作るようにすると、THREE.Spriteはカメラに対して常に正面を向くオブジェクトを生成することができるので、こちらの方が良かったかもしれない。
参考文献
*1:この方法だと、静的なテキストしか配置できないことになってしまうという欠点があるが。
現時点での好きなLinux環境
自分の気に入っている、Linux環境上でのソフトウェア群をまとめてみた。
Desktop Environment
KDE
重厚長大だが、充実した機能とカスタマイズ性が、どのKDEのバージョンでも維持されており、それがKDEの魅力でもある。Qt製で、Qtのバージョンアップに従ってKDEもバージョンアップを重ねており、現行バージョンはKDE Plasma 5である。KDE 4の頃はめちゃくちゃカスタマイズしていたが、KDE Plasma 5になってから面倒になってデフォルト設定のまま使うようになってしまった*1。
Xmonad
マシンスペックによってはKDEはさすがに重すぎるので、計量デスクトップ環境を導入したくなることがある。そのときに選んだのはXmonadだった。キーバインドを覚えなければならないので、触れていないと使い方を完全に忘れてしまうという欠点はある。その一方で、カスタマイズ性が高く、デスクトップを自在に構成することができるのが、大きな利点であり、マウスなどなくとも大抵の操作ができることに感動する。
Linux Distributions - Arch Linux
Linux Distributionの違いを規定する大きな要素の1つにPackage Managerがあると考えており、Package Managerのあり方に、そのディストリビューションのコンセプトが反映されているというように感じる。
Package Manager
apt-get, aptitude, yum, dnf, zypper, portage, pacman, ...., の様々なツールがあるが、pacmanの、公式リポジトリから導入できるパッケージはバイナリとして導入できて、そうでないソフトウェアは、ビルドファイルがAURと呼ばれるコミュニティベースのリポジトリに共有されており、そこからダウンロードする場合は原則として手元でビルドするという仕組みになっているのが、portageのように全てのソフトウェアをビルドするというほど時間がかからず、一方で、パッケージマネージャに管理されていないソフトウェアをインストールするのがそれほど面倒ではない*2ことが、一番気に入っている理由かもしれない。
Shell - fish
ssh環境ではだいたいの場合で、bashがデフォルトなのでそれを使っているが、zsh + oh-my-zshに比べてfishのほうがfish_configによる設定が容易だったので、fishを利用している。fishはカスタマイズの容易性ももちろんのこと、補完が過去の入力履歴をもとに常に先取り的に行われるというのが便利である。bashでもしばしばback-i-searchを使うことが多いので、私の場合のだいたいのコマンド操作は、過去のコマンド入力の繰り返しであるか、その改変にすぎないということが肌感覚で分かったので、fishに移行したときはその便利さに驚いた。
Editor - Emacs
最初のあいだは、頑張ってEmacs Lispで設定ファイルを記述していたが、最近それに倦んでいるので、Spacemacsを使う。
.spacemacs
に設定を記述する。dotspacemacs-configuration-layersは今はこんな感じで書いている。
dotspacemacs-configuration-layers '( yaml (auto-completion haskell :variables haskell-completion-backend 'ghc-mod) ;; better-defaults emacs-lisp git github tmux command-log org spacemacs-layouts (ibuffer :variables ibuffer-group-buffers-by 'projects) semantic cscope gtags latex (ruby :variables ruby-version-manager 'rbenv ruby-enable-enh-ruby-mode t) go (haskell :variables ;;haskell-completion-backend 'intero haskell-process-type 'stack-ghci haskell-enable-hindent-style "johan-tibell") rust scheme python markdown javascript react c-c++ html docker typescript dash ansible sql osx (shell :variables shell-default-height 30 shell-default-position 'bottom) ;; spell-checking syntax-checking version-control )
Other
あとはだいたいこのあたりを使っているが、あとはほとんど設定せずデフォルト状態で使っているので、これ以上細かく述べない。
Google BigQuery の SQL文tips
Google BigQueryとはおおざっぱに言うと、大量のテーブルデータに対して高速にSQLを実行できるフルマネージドサービスである。主にアクセスログや、テーブルデータのように、十分に大きいデータソースを持っている企業にとっては安価で、すぐにSQLを実行することができるため、導入している企業も多い*1。 そのため、企業のログデータ解析をするとなると、まず選択肢に上がるのはこれか、Treasure Data などになるのではないだろか。
BigQueryを実行するにあたっては、SQL文を記述する必要がある。単純にテーブルデータを呼び出すだけの利用目的であれば、RDBMS バックエンドのjsonサーバーを実装するときに書くような、Web エンジニアが普段書くSELECT文で十分なケースも多い。しかし実際には、テーブルを跨いだ集計が必要だったりすると、クエリは長くなってしまい、平気で数百行の SQLになってしまうこともしばしばあある。こうしたSQL文は、初見では読み解くのが難しかったり、フルスクラッチで一から組み立てるのが難しかったりする。
ここでは、StandardSQL の文法のもとで、筆者がBigQueryでSQL文を書く中で気づいた、お役立ちtipsをいくつか紹介したい。
具体例
1. with句を利用してサブクエリをシーケンシャルに記述する
例えば、2つのテーブルA,Bがあって、それぞれに対して絞り込んだものを最終的にJOINしたい、というような場合では、
- テーブルAを絞り込んでできたA'
- テーブルBを絞り込んでできたB'
- A'とB'をジョインしてできたC
のような手順でクエリを構成したい。こうしたものを記述するときに、よくあるのはジョインしたいテーブルの情報を、サブクエリで書き下す場合である。
SELECT * FROM `log` INNER JOIN ( SELECT * FROM `users` WHERE users.id < 1000 ) AS master_users ON master_users.id = log.user_id ;
だが、これが複雑に入り組んでくると、クエリの全貌をとらえるのが難しくなる。そこで、サブクエリの変種であるWITH句を利用して、
WITH master AS ( SELECT * FROM `log` WHERE log.id < 1000 ), master_users AS ( SELECT * FROM `users` WHERE users.id < 1000 ) SELECT * FROM `log` INNER JOIN master_users ON master_users.id = log.user_id ;
と書くと、WITH句によってフローをシーケンシャルに記述することができるので、上から順に読んでいくとどういう流れで処理しようとしているのか、より分かりやすく表現することができる(はず)。
2. 何度も使い回したい数値を関数として使い回したい
SQL文内で関数を定義することもできる。
CREATE TEMPORARY FUNCTION max_user_count() AS ('1000');
これは例えば、先ほどのwith句を利用するような長大なクエリの中で、共通の条件で絞り込みたいといった場合に応用ができる。
3. 1ずらしの連続値が入っているカラムを、joinによってeach_consする
たとえばこんなテーブルを考えてみよう。
id | group_id | seq_id |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 1 | 3 |
4 | 2 | 1 |
5 | 2 | 2 |
このテーブルでは、全体に対してidが付与されているほか、複数のレコードにわたって共通のgroup_idが付与されており、そのgroup_id内でシーケンシャルに増加するidが割り振られている。このとき、同じgroup_idの中で、連続するseq_idのあいだで、どのような変化が起きているか、という1次のマルコフ性を考慮した解析を行いたいということがある。ruby でいうところの、each_cons
に相当するような処理を、BigQueryで記述したいということである。
このとき、seq_idを1ずらしたテーブルを作って、seq_idでjoinすれば良い。具体的には、join前のテーブルをwith句で取ってくる時に、seq_idをデクリメント(ないしインクリメント)して取ってくる。そして、その後にseq_idと、group_idが一致するものをマージするというSQL文になる。
WITH master_1 AS ( SELECT id, group_id seq_id target FROM `sample.log` , master_2 AS ( SELECT id, group_id seq_id - 1 as seq_id, target FROM `sample.log` ) SELECT DISTINCT master_1.group_id master_1.seq_id master_1.target AS target_1 master_2.target AS target_2 FROM master_1 LEFT JOIN master_2 ON master_1.group_id = master_2.group_id AND master_1.seq_id = master2_.seq_id
たとえば上記のようにすることで、target カラムが、seq_idの前後でどう変化したかをみることができるようになる。
4. case whenを利用して一致したら1、不一致なら0を返す
SELECT CASE WHEN type = 'User' THEN 1 ELSE 0 FROM `users`
これを応用して、一致する要素が何件あるかを集計することも可能だ。
SELECT SUM(CASE WHEN type = 'User' THEN 1 ELSE 0) FROM `users`
5. "&=" で結合されたパラメータを別々の行に分ける
URLをカラムに直接格納するとき、しばしば、/query?q=param1&q=param2
のように、アンドで結んだパラメータが入っていることがある。
これを、それぞれのクエリ文字列に分けて取り出して解析をしたいというとき、
WITH log AS ( SELECT * , REGEXP_EXTRACT_ALL(parameters, r'(?:^|&)q=(\d+)') as params FROM `log`), log_param AS (SELECT * FROM log CROSS JOIN UNNEST(log.params) AS param)
上記のように書くことで、param というカラムにもともと複数入っていた複数のパラメータが展開されて、それぞれ1つずつパラメータが別々のレコードに格納されるようになる。これを下流の解析で用いることで、パラメータの文字列ごとの集計などが可能になる。
*1:学生の頃には大金でとても払えないし、そもそもBigQueryが必要とされるほどのデータを集めること自体が難しいので、全く使うことはなかったが。