スマホが割れた日

その瞬間は、前触れもなくやってきた。

いつものように、仕事帰りに烏丸御池の交差点で信号待ちをしていた。ちょうど赤に変わったタイミングで時間があったので、スマホでニュースを眺めていた。信号が切り替わり、そろそろ渡ろうとしてスマホをポケットにしまおうとした瞬間だった。左手と右手がいきなりぶつかり、右手の力が緩み、スマホは宙に舞った。

何が起きたのか自分でもわからなかった。はっと我に返ったら、スマホは硬いコンクリートに打ち付けられていた。

一体左手で何をしようとしたのだろうか、今となってはもう思い出せない。右手に痒みを感じて掻こうとしたのか、スマホを左手に持ち替えようとしたのか、あるいはゴミが入った目を掻こうとしたのか。両手が別の目的を持って動線を描いた結果、衝突してしまった。

精密機器がコンクリートにぶつかる瞬間、明らかによくない音がした。すぐに拾い上げて状況を確認したが、画面には無数のひびが入り、悲惨なものだった。全くなんでもないですよ、最近のスマホは衝撃には強いんですよと、周囲の人に心配されないよう冷静を装いながら横断歩道を渡ったが、画面の割れたスマホを握る手は震えが止まらなかった。

家に帰ってから改めて詳細に観察してみた。画面はもう見るも無残な姿だった。インクを流して蝶の羽根に見立てるのも悪くないアイディアだ、それくらい無数のヒビが走っていた。しかし枠は意外と無傷だった。いや、ほんの少しの傷はついていたが、枠が地面に当たったとは思えなかった。おそらく画面を下にして落下したのだろう。何よりもの救いだったのが、画面が割れた以外は、動作に全く問題がなかったことだ。落ちた直後に拾い上げたときも電源はついたままだったし、画面が割れていてもタッチパネルの操作は通常通り行うことができた。ただ、画面から少し変な油の臭いがした。

暫くの間、様々な思いが頭を駆け巡った。二年間大事に扱ってきたものを、一瞬の不注意で落としてしまったことが悔しかった。あの瞬間、なぜ両手がぶつかってしまったのだろう。反射的に足で受け止めていれば衝撃は和らいだだろうか、本当にインクを流してやろうか、流石にこの見た目はお客さんに会うときには恥ずかしすぎるな、自分でネジを開けて交換して直そうか、細かい紙やすりで削ったらちょっとはマシになるのではないか、新しい機種が出ているわけだし自分で直してうまくいかなければ新しいものを買えばいいんじゃないか。

シャワーを浴びながら様々なことを考えたが、改めて机の上においている無残な画面を見て、餅は餅屋、根は張るかも知れないが専門家に任せるのが一番だと言い聞かせて、修理の予約を取った。なんだ、ジーニアスバーだなんて小洒落た名前をしているじゃないか。

快晴だった。阿蘇山の噴火のニュースを明け方まで聞いていて、よく眠れなかった。2万円までは払う覚悟で銀行でお金を下ろし、阪急電車に乗った。帰省の時くらいしか使わないから、夏の盆以来だった。土曜日の昼間にも関わらず、満席で立っている人も多かった。

いつもならスマホでニュースや技術ブログを眺めるところだったが、画面の割れたスマホを電車の中で見るのはなぜか自分が許さなかった。未成年での飲酒は流石に良くないんじゃないかと無粋なツッコミを入れながら、新海誠の作品を読み耽った。孤立感と憧憬、美しい女性たちの繊細な心情描写にすぐに惹き込まれていった。

梅田は久しぶりだった。いつも帰省のときには十三で折り返してしまうし、そもそも梅田に用事があるなんてことはなかった。もしかしたら就活でスカイビルに来て以来ではないか。そう思った瞬間、就活の時の様々な嫌な思い出が蘇り、慌てて心から追い出した。もうあの頃の自分とは違うんだ。

予約時刻までしばらく時間があったので、ヨドバシで暇を潰すことにした。京都でもそうなのだが、ヨドバシに入るとまず必ず地下一階に降りる。ラップトップなんてめったに買わないものだし相場感覚が分からなくなるものなので、たまにはこうしてスペックや価格を眺めて歩くのだ。SONYのラップトップを片手で持ち上げてみたり、こんな薄いものに未だにLANポートを作る技術に感心したり、いつまでも変わらないメモリー容量に落胆したりした。少し奥まったところに自作用のパーツも並んでいた。自作PCというのは作ったことないのだが、ラップトップのHDDをSSDに交換したときから、部品の価格にも目を向けるようになった。

京都のカメラコーナーは一階なのに、梅田は二階なんだなと呟きながらエスカレーターを登る。特に買いたいものはないのだが、唯一持っているNicon 1用のレンズは欲しくなることがある。財布に入っている現金で買えるのではないかという邪念を振り払い、カメラコーナーを後にする。途中で円偏光フィルターが目に止まった。大学で量子力学をやっていたものだから、偏光板や波長板などは研究室に身近にあった。円偏光を直線偏光にして偏光板を通して… あんなに一所懸命研究したのにだんだん記憶があやふやになっている。必死にBloch球に右円偏光と左円偏光を配置する。大丈夫だ、まだ思い出せる。このフロアの中でこのフィルターを量子現象として説明できる者は他にはいまい。なぜか勝ち誇ったような気分でヨドバシを後にした。

f:id:itchyny:20161008135759j:plain

陽射しはいよいよ増していた。道端に銀杏が落ちていなければ、夏かと勘違いするような暑さだった。タワークレーンにレンズを向けてみたが、こんな陽射しの中じゃ新海誠にはならないな、そんなあたり前なことを思いながら南下した。地下鉄で6分だと書いていたので、徒歩でのんびり行くことにした。

f:id:itchyny:20161008141221j:plain

大阪の地理には疎い。神戸に生まれ育った身として三宮の商店街の店には多少詳しいのだが、なんせ大阪まではでてくる機会すらなかった。だから心斎橋とか難波とか言われても位置関係はわからないし、大阪城にはどうやって行けば辿り着けるのかも知らない。しかし、今日の目的地はとにかく御堂筋を歩いていけば着くはずだ。道路標識が、和歌山へと続く道だと主張していた。陽射しの中で歩いていると気分も明るくなり、和歌山には行ったことないな、いつか行ってみるか、そういう気分になった。

大江橋淀屋橋を超えると金融街に入る。銀行支店が道に並ぶ。伏見町はここにもあるのか、水が良いところには酒蔵があると聞くがここはどうなのだろうか、道修町というのはどうしゅうではなくどうしょなのか、難読だなぁとか、1ブロック渡るたびに新しい発見がある。

f:id:itchyny:20161008143044j:plain

本町通を超えて阪神高速をくぐると、また街の景色が変化する。フランク・ミュラーエルメス、スワロフスキー、オメガ、カルティエルイ・ヴィトン。畏れ多くも店の入口に店員が待機していて入ろうものなら扉を開けてくれる親切なお店が立ち並ぶ。夏なんかはあそこに立つのは暑いだろうなぁ。スーツくらいきちんと決めていつか行きたいものだな。そんな風に思って通り過ぎていると、短パンジーンズ麦わら帽子の兄ちゃんが彼女らしき人物を連れて出てきたりしてずっこけそうになる。

心斎橋はおそらく生まれて初めて来た場所だ。高級ブランド店の並ぶ北側よりも、心なしか南側のほうが活気があるようにみえる。人の交通をうまく交わしながら、目的の店へと足を運ぶ。

f:id:itchyny:20161008171021j:plain

店内は混雑していた。店員の数と客の数が1:1くらいなのではと思うくらい、店員がたくさん働いていた。それでもどの店員に声をかけたらいいものかよく分からず、しばらくうろうろしていた。サポートは二階だと聞かされ、オシャレな螺旋階段を登った。

二階も同様に混雑していた。なぜか電源がつかないとかパスワードが分からないとか様々な事情でここに来るんだろう。一時間ほど歩いてきたので少し汗をかいており、座るのが憚られた。しかし、予約時刻を過ぎてもなかなか呼ばれず、貧血で倒れそうになり、仕方なく隅の方に座らせてもらった。

しばらく観察していたが、様々な人が働いていた。男性で長髪の人も、背が高い人も、韓国語や中国語が流暢な人も、足の不自由そうなスタッフもいた。上はみなスタッフのシャツを着ていたが、下はジーンズが多かった。そういう企業文化なんだろうなぁ、いいことだと思った。それにしても少し混雑しすぎていないだろうか。

対応してくれたのは、自分と同じくらいの背丈の男性だった。どうやら3500円ほどで画面の交換をしてくれるらしい。2万円は覚悟していたから、かなりありがたかった。一時間後にできますと言われたので、再び街に散歩に出かけた。用を足したかったので東急ハンズに入り、申し訳程度にボールペンを購入した。一階はすっかりハロウィン模様だった。作りたてのポップコーンの臭いに吐き気を感じながら、その場を離れた。

心斎橋は東西に地下街が発達していた。もう夕方だが今日は朝食をとったきりだったし、一時間ほど歩いたので空腹だった。昼夜兼用にしてしっかり食べよう。学生の頃なら牛丼屋で済ませていたんだろうな。そんなことを考えながら、店を選ぶ。西の端から東の端はかなり距離があり、地下街を歩くだけでも疲れてしまった。地下街の方向と東西南北が数度ずれているのがとても気になった。

結局、東端にある洋食屋を選ぶことにした。少し照明を落としており、こじんまりとした店だった。セットを頼んでから、また新海誠を読み始めた。いい具合の照明と、店内にかかるショパンのワルツが、物語を趣深く装飾してくれて最高の気分だった。中華料理屋を選ばなかった自分を密かに褒め称えた。ハンバーグがとても美味しくて、どうやって作ったのか聞きたいくらいだった。ご飯はおかわりできると案内されたが、ガツガツ食べる気分でもなかったので遠慮してしまった。

人通り忙しない商店街を抜けて、また店まで戻ってきた。数分したら、修理されたスマホが出てきた。間違いなく自分のスマホだったし、それはもう見事だった。素人が開けるときにつけがちなドライバー傷もなかった。保険がなかったら1万を超えていたという。これを修理してくれた技術者は、経験を積み、腕の立つ、顧客の製品を修理するという責任を負うことができる人間に違いない。それは素晴らしい仕事だ。対応してくれた人に対価を払いたい。保険は対価報酬の関係を不確実なものにする。そんなわけの分からない思考も、御堂筋に吹く秋風に飛ばされて、心は澄み切っていった。

もう梅田まで歩く体力は残っていなかったので、帰りは御堂筋線に乗った。ホームボタンが少し固くなっていたり、タッチパネルの滑りが少し悪くなっていることも、部品が新品である証拠だった。あのヒビの入った画面とはもうおさらばなのだ。いつもは必死にタイムラインを消化するのだが、そんな気力もなかった。

阪急京都線に乗り座席につくと、疲れがどっと出てきた。対応してくれた店員の笑顔、一週間は予約がいっぱいだと聞かされて唖然としているおじさん、洋食屋で手をそっと添えてお釣りを渡してくれた店員、修理を待つために店内で座り込んでいた中国人たち、限られた時間内で修理しなければいけないという使命を負って仕事をしている技術者たち、高級宝石店の扉の向こうに立っていた執事のような人たち。色々な人生や考えを想像し、その立場に立って見える風景を思い描きながら、電車の心地よい揺れに誘われ、眠りに引き込まれていった。

Vim 8.0 リリース!

Vim 8.0 released!

Vim 8.0が先ほどリリースされました。10年ぶりのVimのメジャーバージョンアップです。

Vimのバージョンをcronで毎日上げ続け、最新のパッチを確認し続ける日々を送ってきました。そして、今日も夜11時のcronでバージョンが上がりました。新しいメジャーバージョン、8.0でした。

ここ一年はVimにとって様々な重要な機能が入りました。JSONエンコーダーとパーサー、パッケージ機構、channelとjob、タイマー、ラムダ式など、プラグイン製作者にとって大事な機能ばかりです。今後、より高度なプラグインがでてくることでしょう。これらの機能に対する日本人の貢献は素晴らしいものです。

リポジトリGoogle codeからGithubに移動するという重要な決定も行われました。この決定の過程にも、vim-jpの皆さんが深く関わっています。私は傍から応援することしかできませんでしたが、皆さんの貢献によりVimの開発が更に活発になっていくのを嬉しく思いました。

新機能のうちすぐにユーザーが設定したり使用したりできる機能もたくさんあります。 set breakindent を設定しましょう、インデントのある長い行の折り返しの見た目が美しくなります。検索中に<C-g>, <C-t> を押してみましょう、カーソルはコマンドラインのまま、ハイライトを移動することができます。ビジュアルモードのg<C-a>, g<C-x>で連番を作れるようになりました。便利な機能がいっぱいです。

個人的に嬉しかったのは、getwininfo()でlocation listかquickfix listか判別できるようになったことです。この2つはコマンドが全く違うのに、これまで正しく判別する方法がありませんでした。これからは自信を持って、これらを区別するコードを書くことができます。

また、最近入ったScalaのサポートも嬉しいものでした。私はVimScalaを書いています。プラグインを使えば何不自由なく書けるのですが、やはりVimに正式にScalaのサポートが入った (有名なScalaプラグインが本体に取り込まれた) ことは大事なことです。Scalaを書く道具を迷っている人に、胸を張ってVimという選択肢を提案することができます。

最新のパッチをずっと追いかけている人にとっては連続した道の通過点に過ぎません。8.0がリリースされた後も、矢継ぎ早に8.0.0001, 8.0.0002とパッチは上がり続けています。しかし、メジャーバージョンを上げるというBramの決意には、こんなにも素晴らしい新機能が入ったVimをもっと多くの人に使って欲しいという思いが感じられます。

私はVimを開発されている皆さんが好きです。Vimプラグインを書かれている皆さんが好きです。Vimを使っている皆さんが好きです。

そしてVimが大好きです。新時代の幕開けに、祝杯を。

JavaScriptのsetTimeoutをログに出す

setTimeoutは難しい。いつ呼ばれるかよく分からないし、ライブラリーを使うとそのライブラリーがsetTimeoutを使いすぎてしまう。よく分からなかったけどsetTimeoutすると動くからそうしていた、んだけど実はタイミングの関係で偶然うまく動いているように見えているだけだった、なんてこともよくある。

ウェブアプリケーションの描画が遅い。「なぜか遅い」が、処理を丁寧に追っていっても手がかりがつかめないということがある。色々な方法を駆使した後に、なぜかsetTimeoutの発火が遅いということにたどり着いた。どれくらい遅いか。

window.setTimeout = (function(setTimeout) {
  return function() {
    var handler = arguments[0];
    var wait = arguments[1] || 0;
    var log = function(action, color) {
      var d = new Date();
      console.log('%c%s%c [%c%d%c/%c%d%c] (%s %s) %s', 'color:' + color, action, '',
                  'color:blue', (Date.now() - registered), '', 'color:blue', wait, '',
                  d.toLocaleTimeString(), d.getMilliseconds(), handler.toString().slice(0, 100));
    };
    var args = [].slice.call(arguments, 2);
    var registered = Date.now();
    args.unshift(function() {
      log('FIRED', 'red');
      handler();
      log('DONE', 'blue');
    }, wait);
    log('REGISTERED', 'green');
    return setTimeout.apply(null, args);
  };
})(window.setTimeout);

setTimeoutをログに可視化してみた。コードの中で使っているあらゆるsetTimeoutのログが出力される。 [X/Y] みたいな表示がされるが、 Y がsetTimeoutの第二引数で、 X がsetTimeout登録からの経過時間を表している。

f:id:itchyny:20160822124018p:plain

ああ、最悪だ。setTimeout 0のつもりが2秒も後に発火されている事がわかった。

本来ならば setTimeout(f, 500);REGISTERED [0/500]FIRED [500/500]DONE [530/500] のようになるのが正しい (f()に30msかかる時)。ブラウザー描画処理が入る可能性があるので、setTimeout 0でもFIREDが [20/0] とかになることはある。しかし、上の画像のように2秒も遅れていると「何かがおかしい」ということが分かる。

setTimeoutが可視化されたのは良いが、実際の原因は別のところにあることが多い。setTimeoutが意図しているよりも遅れるということは、そこで別の重い処理が走っているということになる。その処理とは、自分の書いたJavaScriptの処理だったり、大きなDOMの挿入、ブラウザーレンダリングだったりする。あとは、ブラウザーのTimelineやProfilesといった普通のデバッグ手法をとる。

console.logで時刻を出すというのは確かに原始的なやり方だ。printfデバッグと同じである。しかし、setTimeoutのタイミングが目に見えるのはとても便利だ。以前作ったsjspも同じように古典的な手法だが、今でも重宝している。

実行処理系のデバッガと、古典的な手法を行ったり来たりして原因を特定していくというのは、どの言語でもある気がする。どちらも良いところと悪いところがある。デバッグ技・プロファイル技は職人芸になりがちだが、文章にして伝えにくいところもあるので、どうしても特定の人にスキルが偏ってしまう。うまく伝えていきたい。

CSVファイルをSQLのクエリで集計できるqコマンドをHaskellで実装してみました!

先日、Twitterでqコマンドが話題になっていました。 github.com スターが3000を超えていてすごいですね。2014年から開発されているツールで、Pythonで書かれています。

これはGoで実装してみたいなーと思っていたところ、mattnさんが素早く実装されていました。 mattn.kaoriya.net 一本取られたと思ったものの、よく読むとまだ標準入力しか対応していないようです。

いったいどういう仕組みなのか、何の実装が難しいところなのか、qコマンドが嬉しい場面はどういうケースなのか、自分も知りたくなったので1から実装してみました。 私が一番素早く書ける言語ということでHaskellを選びました。

qhs

qコマンドのHaskell実装、ということでqhsと名づけました。 github.com stackが入っていればインストールは簡単です。

 $ git clone https://github.com/itchyny/qhs
 $ cd qhs
 $ stack install
 $ export PATH=$PATH:$HOME/.local/bin
 $ qhs "SELECT 100+200"
300

基本的な使い方

qコマンドの基本思想は、CSVのようなファイルをSQLのテーブルとみなしてSELECTできることです。 qhsコマンドもqコマンドと同じように動きます。 SELECT * FROM [ファイル名] が基本の形です。

 $ cat basic.csv
a0,1,a2
b0,3,b2
c0,,c2
 $ qhs "SELECT * FROM basic.csv"
a0 1 a2
b0 3 b2
c0  c2

カラム名は自動的に c1, c2 ... のようになります。c2がNULLではない行だけにしてみます。

 $ qhs "SELECT * FROM basic.csv WHERE c2 IS NOT NULL"
a0 1 a2
b0 3 b2

数字の列の空文字はNULLになるようになっています。例えば、二行目の平均を取ることができます。

 $ qhs "SELECT avg(c2) FROM basic.csv"
2.0

1と3の平均なので2です。

一番上の行にカラム名がある時は、-H (--skip-header) 引数を与えてあげてください。

 $ cat basic.csv
foo,bar,baz
a0,1,a2
b0,3,b2
c0,,c2
 $ qhs -H "SELECT foo,baz FROM basic.csv"
a0 a2
b0 b2
c0 c2

普通のSQLと同じように、JOINしたりUNIONしたりすることもできます。 例えば、先月と先々月の家計簿CSVから高かった買い物トップ10を表示してみます。

 $ qhs -d , -O -H "SELECT * FROM 家計簿06.csv UNION SELECT * FROM 家計簿07.csv ORDER BY 金額 DESC LIMIT 10"
日,時刻,金額,用途,場所
27,0000,66000,家賃,銀行振込
27,0000,66000,家賃,銀行振込
30,0000,8200,端末料金,サンプルコミュニケーション
30,0000,8200,端末料金,サンプルコミュニケーション
16,2200,7948,親ビールギフト,サンプルショッピング
5,1542,5690,ポロシャツ・ベルト他,サンプル洋服店
25,2245,5300,会社飲み会,いつもの飲み屋
2,2215,5000,同期と飲み会,四条の飲み屋
25,1913,3839,食料品,サンプルマート
3,1440,3740,散髪,サンプルサロン

とても便利ですね。そんなに大きな買い物はしてないことが分かって安心です (家計簿はサンプルです・私の実際の出費とは関係ありません)。

CSVファイルをSQLのテーブルのように扱い、JOIN・UNION・サブクエリによって集計する、これがqコマンドの真髄なのだと思います。 (mattnさん頑張ってください…)

標準入力

qコマンドやqqコマンド、そしてqhsコマンドは標準入力からテーブルを読み込むことができます。

 $ cat basic.csv
foo,bar,baz
a0,1,a2
b0,3,b2
c0,,c2
 $ cat basic.csv | qhs -H "SELECT foo,baz FROM - WHERE bar IS NOT NULL"
a0 a2
b0 b2

-は標準入力から読み込むことを表しています。

他のUNIXツールとの合わせて様々な集計を行うことができます。 例えばwcコマンドの行数でソートしたり

 $ wc * | qhs "SELECT c4,c1 FROM - WHERE c4 <> 'total' ORDER BY c1 DESC"
Main.hs 118
File.hs 66
Option.hs 61
Parser.hs 51
SQL.hs 45

psコマンドと合わせたりなど、組み合わせは自由自在です。

 $ ps -ef | qhs -H -O "SELECT UID,COUNT(*) cnt FROM - GROUP BY UID ORDER BY cnt DESC LIMIT 3"
UID cnt
503 102
0 86
89 3

このpsの例はqコマンドのExampleから拾ってきたものです。 よく考えられていますね、脱帽しました。

実装基礎

qコマンドを自分の好きな言語で実装したいと考えたとします。 実装のイメージは湧きますか? SQL相当のクエリ実行器を作らないといけないのかと嫌な予感がよぎるかもしれませんが、そこまで苦労する必要はありません。

モリー上にデータベースを作り、ファイルを読み込んでテーブルを作り、そこにクエリを投げる、これが基本的な構成です。 これだけで理解できた人は以下は読まなくてよいでしょう。

qコマンドもmattnさんのqqコマンドもSQLite3のオンメモリデータベースを用いています。qhsもこれに倣ってSQLite3を使っています。初めて触るライブラリーでしたが、インターフェースが分かりやすいのですぐに使えました。

テーブルの構築

qコマンドの実装で最も難しいのはここだと思います。 qhsを実装するときも、かなり頭を捻りました。

例えば、次のような入力を考えます。

 $ qhs -d , -O -H "SELECT * FROM ./家計簿06.csv UNION SELECT * FROM 家計簿07.csv ORDER BY 金額 DESC LIMIT 10"

この「クエリ」自体は、SQLのクエリとして実行できるものではありません。 また、標準入力からの読み取りという簡単なクエリでさえ、通常のテーブル名としては不正なものになっています。

 $ wc * | qhs "SELECT * FROM -"

おそらくどんな言語でもSQLライブラリに突っ込んだらエラーになるでしょう。

まずは、この「クエリ」からファイル名を抽出します。 例えば ./家計簿06.csv とか 家計簿07.csv とか - のようなものです。 ファイルはテーブルに相当しますから、おそらくFROMJOINの後にくる単語がそうでしょう。 そして、このファイル名を有効なテーブル名に置換します。

SELECT * FROM temp_table_0 UNION SELECT * FROM temp_table_1 ORDER BY 金額 DESC LIMIT 10

テーブル名はなんでもよいです。 ただ、以下のことが大事です。

  • ファイル名との対応をきちんと持つこと (後で使う)
  • あるファイル名に対しては一意に定まること
  • SQLのテーブル名として正しいものであること

また、ファイル名と同じくらいの長さが好ましいと思います。 SQLのパーサーのエラーメッセージを元に戻してユーザーにフィードバックする時に、エラー位置がずれないようにするためです。

今回の例では、

"./家計簿06.csv" => temp_table_0
"家計簿07.csv" => temp_table_1

という対応が取れました。 標準入力のみの簡単なクエリでは、例えば次のような形になるでしょう。

"-" => temp_table_0

temp_table_ の部分は何でも構いません。 qhsの実装では、ファイル名をSHA1エンコードしたものを使っています。 少しトリッキーですが、SHA1を使っておけば入力に対して安定します。

さて、次にやることは、このファイルたちを読み込んで、テーブルを作ることです。 コマンドの引数の区切り文字に従って、ファイルを読み込みます。 この段階でカラム名が決定しますので、CREATE TABLEできるようになります。 数字のカラムを自動判別して型を数字にしておくとか、gzipされたファイルを読み込むときにはdecodeするみたいな細かい芸はいろいろあります。

基本的には、一行ずつ読み込み区切り文字で分割していけばいいわけです。 ただしカラム数を超える分は分割してはいけません。 例えばps -efの結果があったとして

  UID   PID  PPID   C STIME   TTY           TIME CMD
    0     1     0   0 月10AM ??        40:25.50 /sbin/launchd
    0    45     1   0 月10AM ??         3:26.75 /usr/sbin/syslogd
    0    46     1   0 月10AM ??         1:15.17 /usr/libexec/UserEventAgent (System)
    0    48     1   0 月10AM ??         0:34.95 /usr/libexec/kextd
   55    54     1   0 月10AM ??         0:00.64 /System/Library/CoreServices/appleeventsd --server
  501  1484     1   0 月10AM ??       301:08.31 /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    0 16451     1   0 水10AM ??         0:00.08 /usr/libexec/syspolicyd
  501 98979 98977   0 土02AM ttys013    0:00.14 -zsh

スペースによって分割しないといけませんが、CMDのカラムを必要以上に分割してはいけません。 もし Google Chrome がカラムで分割されてしまったら、次のような基本的なLIKE文も動かなくなるでしょう。

 $ ps -ef | qhs -H "SELECT * FROM - WHERE CMD LIKE '%Google Chrome%'"

UNIXツールの扱いやすいように見えてなんか扱いにくい出力には、いつも頭を悩まされます。 だからこそqコマンドも一定の支持を得られてきたのだと思います。

さらに、CSVはダブルクオートでセルが複数行にまたがることがあります。ダブルクオートの中のダブルクオートはダブルのダブルクオートで表現するらしいです。

 $ cat multiline.csv
foo,bar,baz,qux,quux
a0,1,"a2
b0"",3,""b2
c0",,c2
 $ qhs -d , -H -O "SELECT foo,bar,quux FROM multiline.csv"
foo,bar,quux
a0,1,c2

bar1baz"a2\nb0\",3,\"b2\nc0"qux""quuxc2なので、これで合っています。 めでたしです。 律儀にもqコマンドがこのケースに対応しているのでqhsでも対応してみました。 最悪です。

なんやかんやで入力を読み込むことができれば、一行ずつテーブルに流し込んでテーブルの準備は完了です。

クエリの実行と結果の表示

テーブルの準備ができれば、後はクエリを実行するだけです。 ここで言うクエリとは、ファイル名を置換した後の、ちゃんと実行できるクエリのことです。

qコマンドでは出力の区切り文字を指定することができます。qhsも同じインターフェスにしてみました。 例えば、入力ファイルはカンマ区切りだけど、出力ファイルはタブ区切りにしたいという時は次のようにします。

 $ qhs -d , -D $'\t' -H "SELECT * FROM basic.csv"
a0  1  a2
b0  3  b2
c0     c2

dはdelimiter (区切り文字) の頭文字です。小文字 -d が入力で、大文字 -D が出力です。 いちいち $'\t' と書くのは面倒なので、 -T を使うこともできます。 -T-D $'\t' は同じです。 -t-d $'\t' も同じです。

テスト

qhsはHaskellで書かれていますので、テストもHaskellのテストツールを使います。 HspecHaskellにおける標準的なテストフレームワークです。 Rspecにインスパイアを受けて作られたフレームワークです。

一行を特定のカラム数で分割する関数や、クエリのファイル名を置換する処理は難しいので丁寧にテストしています。 テストの中でファイルを読んだり書いたりすることも可能です。

コマンドラインツールですから、ビルド結果のコマンド自体が正しく動くことをテストするのも大事です。 テストのディレクトリーには、シェルスクリプトと、期待されるの出力ファイルを置いています。 これらをHspecの中で実行して、その出力と期待される出力ファイルを比較するというテストを書いています。 こうしておけばテストを追加するのも簡単ですし、最悪Hspecが滅びたとしてもシェルスクリプトを実行し出力を比較することができれば、どんなテストフレームワークでも生き残るはずです (いざとなったらシェルスクリプトでテストを回すこともできる)。 以前、Go言語で迷路コマンドを作った時も同じようなテスト構成を取りました。 itchyny.hatenablog.com

Travis CI上のstackでのテストはstackのドキュメントを参考にしています。

今回は、qコマンドという元の実装が存在します。 qコマンドのテストがかなり役に立ちました。 qコマンドのリポジトリにある112個のテストのコマンドをqhsに置き換えて、テストの実行と実装を繰り返しながらテストカバレージを上げていきました。 実装する必要があるのかよく分からないオプションがあったりするので、完全互換性を目指しているわけではありません (qコマンドの置き換えテストの中で通るのは4割ほどですが、これでもわりと通っている方だと思います)。

コマンドライン引数パーサー

Haskellにおけるコマンドラインの引数パーサーにはいろいろなライブラリーがあります。 今回は、optparse-applicativeを使ってみました。

Control.Applicativeの演算子と親和性がよくてコードが書きやすいです。 何よりも、オプションの性質を表す Mod f a という型が、Monoidのインスタンスになっていることがイケてます。 Monoidになっているということは、 (<>) で追加できますし、機能を足すのも引くのも簡単です (Haskellを書く時にMonoidのイメージはとても大事です)。 用途は思いつきませんが、オプションパーサーを動的に構築するのも簡単です。

おわりに

CSVファイルをSQLのクエリで集計できるqコマンドを、Haskellで実装してみました。 複数ファイル、標準入力、区切り文字の設定、gzip対応など、基本的な機能は揃っていると思います。 できるかぎり、オプション名などインターフェースはqコマンドに寄せています。 github.com

コメントやimport文を除くと300行程度です。 土曜日に「よし書こう」と思い立ってから丸一日で実装できるサイズでした。 やはりこの言語は楽しいなぁと感じました。 好きな言語は書いていて楽しいし、実装できたときの疲労感も心地よいものです。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者:Miran Lipovača
  • 発売日: 2012/05/23
  • メディア: 単行本(ソフトカバー)

コマンドラインツールの作り方、SQLのクエリのパースの仕方、そしてDBの触り方など、qコマンドには興味深い課題が詰まっていました。 SQLのクエリの構文木からテーブル名を抜き出す処理には、sjspコマンドでもお世話になっているData.Genericsの知識が役に立ちました (ラフにテーブル名を置換した後、さらにバリデーションで厳格なSQLパーサーを通しています)。 itchyny.hatenablog.com 構文木からの抽出や変換というタスクではData.Generics.Schemesの関数が驚異的な威力を発揮します。

DBのライブラリーを触ったり、optparse-applicativeを初めて触ってみたり、コマンドラインツールのテストについて改めて考えなおしてみたり、あるいはそのテストをTravis CI上で実行するまで持って行ったりと、いろいろと楽しいチャレンジだったなと思います。

さて、あなたの好きな言語は何ですか? 次は、あなたがqコマンドを好きな言語で書く番ですよ。

エンジニア募集

はてなでは、好きな言語はとことん好きだ!そんな情熱と愛情あふれるエンジニアを募集しています。

Go言語でbase58コマンドを作りました

Go言語でbase58コマンドを書きました。コマンドラインツールだけではなくて、Goのパッケージとして作っています。

github.com

仕事の関係でbase58を扱うことがあり、base64コマンドのように素早くコマンドラインで変換できるツールが欲しくなったので作りました。ご自由にお使い下さい。

コマンドラインツールは次のように使うことができます。

 $ go get github.com/itchyny/base58-go/cmd/base58
 $ base58
100000000
9QwvW
79228162514264337593543950336
5QchsBFApWPVxyp9C
^D
 $ base58 --decode # or base58 -D
9QwvW
100000000
5QchsBFApWPVxyp9C
79228162514264337593543950336
^D
 $ echo 100000000 | base58
9QwvW
 $ echo 9QwvW | base58 --decode
100000000
 $ echo 100000000 | base58 --encoding=bitcoin
9qXWw

既存のパッケージは出来が微妙でした。Go言語のbase58ライブラリはtv42/base58があるのですが、encodingがFlickr固定になっていたり、pull requestが何か月も放置されていたり、0から始まる数字の時の挙動がおかしかったりします。0から始まる数字の時の問題はパッケージのインターフェースの問題であり、簡単なpull requestで直せるものではなかったので、新しくパッケージを作らざるを得ませんでした。

引数は16進数のほうが良いのか、あるいはbig-endianでencodeされたbinary列のほうが良いのかとかいまいち分かってなくて、とりあえずコマンドラインツールとの結合がやりやすい10進表現のbyte列にしてしまいました。Bitcoinはよく分からないけど16進表現が使われているらしいので、要望があれば用意するかもしれません。

バグや機能要望はGitHubのissuesからお願いします。