Cmd-Return
で全画面にできるのでそのようにしていたけれども、実際のところターミナルが全画面で表示できていて嬉しいことってあまりなかったので左半分に出すようにした。
上半分ではなく左半分にしているのは、ターミナルで見たい情報は幅よりも高さがあることが多いから *1。
*1:ログなど
この記事は、はてなデベロッパーアドベントカレンダーの19日目の記事です。
昨日は、id:t_kyt による あれから一年、あの TypeScript プロジェクトは今 - 多幸感 でした。
はてなの id:aereal です。アプリケーションエンジニアとして日々、サービスの開発に携わっています。
はてなではサービス開発合宿が年に一度ほどのペースで開催されています *1。今年もつい先日開催されました。
私たちのチームは技術的な挑戦を行う一方で、プロトタイプではなく初期実装となるプロダクトを作り上げることを目的として開発に取り組みました。
技術的挑戦について具体的にあげると、大きな視点ではサーバサイドを Go, クライアントサイドを TypeScript で書くことです。それも2日半で初期実装たりうるクオリティを最低限担保してのことです。
Go や TypeScript は既にプロダクションで採用されていますが、はてなにおけるチームすべてに浸透しているわけではありません。 実際、Go や TypeScript でプロダクションのコードを書いた経験がないメンバーが私たちのチームにもいました。
その他にもミドルウェアの選定やアーキテクチャの採用において、勢いを保ちつつもプロダクションへの投入を見据えてチーム全員で検討を行いました。
この記事では、今年の開発合宿で取り組んだ Web アプリケーションにおいて、ボタンの連打を効率よくかつ体験を損うことなく実現するために工夫した点について紹介します。
(なお、以下で紹介する Web アプリケーションは開発中のものであり、一般に公開されておりません。公開の予定も未定であることをあしからずご了承ください。)
私たちが取り組んだサービスでは、ページ上にボタンが存在します。クリックするとページに対して簡単なフィードバックのようなものがつけられます。 つけたフィードバックをあとから消すこともできます。 ちょうどはてなスターのようなものです。
ボタンがあれば連打したくなりませんか? *2
アプリケーションを作り提供する側としては、しばしばそのような業にも似た性は深刻な問題になりえます。
素朴に実装すると次のようなコードが考えられます:
function createFeedback(event) { return $.ajax({ // ... }); } const buttons = document.querySelectorAll('button[data-add]'); Array.from(buttons).forEach((button) => { button.addEventListener('click', createFeedback); });
これではあまりに素朴すぎ、いくつかの問題があります。
特に大きなものとして、クリックごとに HTTP リクエストが発生する、という問題は無視できません。
この課題に対して実際にどのような対策を行ったのか、サーバサイドとクライアントサイドについてそれぞれ紹介します。
連打という文脈から少しずれますが、まずアーキテクチャにおける検討を紹介します。
アプリケーションのアーキテクチャを検討する際、「あるページにつけられたフィードバックのようなもの」をどのように保存するかをまず検討しました。
ユーザ (主体) * ページ (対象) * クリック (行動) というオーダーになるため、注意深く設計しないとレコード数が爆発したり、パフォーマンスを保つことがむずかしくなります。
そこで私たちは Event Sourcing と呼ばれるアーキテクチャをとることにしました。
詳細としては、ボタンをクリックすると HTTP リクエストが発生し、「フィードバックをつけた」というイベント *3 を表現するリソースをサーバへ送ります。 削除する際は、「このフィードバックを削除する」というイベントを表現するリソースをサーバへ送ります。
サーバでは、送られてきた「フィードバックをつけた」「フィードバックを消した」というイベントを表現するリソースを一旦、そのまま保存します。 サーバから表示すべきフィードバックの情報を返す際は、「つけた」「消した」というイベントを集めて計算した値を返します。
加えて、ある時点におけるスナップショットを保存することで計算するときにも効率化を図っています。この時、スナップショットをとった時点より古いイベントのレコードを削除することで、レコード数の増加を緩やかに保つことができます。
こうしたアーキテクチャにより効率よくデータを保存しつつ、データの書き込み時に担保すべき一貫性の範囲を小さく抑えられます。またパフォーマンスの低下も抑えることが期待できます。
素朴な RESTful アーキテクチャでは、ひとつのリクエストでひとつのリソースを作るような実装がとられがちです。
例:
POST /users HTTP/1.1 Content-Type: application/json { "name": "aereal" }
これでは効率が悪いため、ひとつのリクエストに複数のリソースを含めて作成できるように実装しました:
POST /users HTTP/1.1 Content-Type: application/json { "users": [ { "name": "aereal" }, { "name": "noreal" } ] }
サーバサイドが複数のリソースを受け取れるようになっても、先にあげた click
イベントのハンドラで都度 HTTP リクエストを送るような素朴な実装のままではリクエストあたりに含められるリソースは1つのままです。
そこで click
イベントをバッファにためてまとめて送るように実装を変更しました:
function bulkCreateFeedback(events) { const resources = events.map(e => { /* some transform */ }); return $.ajax({ data: { resources: resources }, // ... }); } const buttons = document.querySelectorAll('button[data-add]'); let buffered = []; function onClick(event) { buffered.push(event); } Array.from(buttons).forEach((button) => { button.addEventListener('click', onClick); }); setTimeout(() => { if (buffered.length > 0) { bulkCreateFeedback(buffered); buffered = []; } }, 500);
これで HTTP リクエストは500ミリごとに高々1回まで抑えられます。
バッファリングの詳細から離れて、アプリケーションコードを書く側から見ると「T[]
を受け取る関数」を「T
を受け取る関数」に変換している、といえます。
そこでこの素朴な関数の変換のみを行う accumulate-call という NPM モジュールを公開しました。
README から使い方を引用します:
import { accumulateUntil } from 'accumulate-call'; document.body.addEventListener('click', accumulateUntil((events) => { console.log(`Clicked ${events.length} times`); }, 1000));
ずいぶん簡潔になりました。見た目は素朴にイベントハンドラを与えているのと大差ありません。
また accumulate-call は TypeScript で書かれています。 型定義ファイルを NPM パッケージに含めているため、TypeScript 1.6 以降を使用している場合は DefinitelyTyped などの外部のリポジトリを参照することなく accumulate-call の型定義を tsc に伝えることができます。
参考: Typings for npm packages · Microsoft/TypeScript Wiki · GitHub
はてなでは、レイヤを横断して体験をよりよくする気概のあるエンジニアを募集しています。
(なお応募フォームは連打せずに1回だけ送信ボタンを押してください)
明日は id:hakobe932 です。
*1: サービス開発合宿 - Hatena Developer Blog
*2:私は連打したくなります。
*3: 特にことわりがない限り、「イベント」は DOM Event を、「リソース」は REST アーキテクチャスタイルにおける文脈のリソースを指すものとします。
だいたい欲しいものは実装したので覚悟を決めるという意味での 1.0 リリース。
Perl の値 (スカラ値、ハッシュリファレンス、配列リファレンスなど) が JSON のどの型であるかを推論するモジュール。
Perl の値から JSON の型を推論するモジュールを書いた - Sexually Knowing
与えられたデータから型を推論し、構造を再帰的に辿りながら、JSON Schema の雛形を生成する。
こういう感じでデータを与えると:
my $generator = JSON::Schema::Generator->new; $generator->learn({ id => 1, name => 'yuno', }); $generator->learn({ id => 2, name => 'miyako', }); $generator->learn({ id => 3, name => 'sae', accessories => ['eyeglass'], }); my $schema = $generator->generate;
こういう雛形が得られる:
{ "$schema" : "http://json-schema.org/draft-04/schema#", "description" : "TODO", "properties" : { "accessories" : { "items" : { "example" : "eyeglass", "type" : "string" }, "type" : "array" }, "id" : { "example" : 1, "type" : "number" }, "name" : { "example" : "yuno", "type" : "string" } }, "required" : [ "id", "name" ], "title" : "TODO", "type" : "object" }
array だったら items.example に代表値を置くとか、けっこうおもてなしできていると思う。
null かもしれない値を "type": ["null", "string"]
のようにするのがいいのか、required
から外すだけでいいのか、もうちょっとユースケースを見極めたい。
Minilla を使っている。minil new
してからちょっと手を入れる。
<aereal@aereal.org> <aereal@users.noreply.github.com>
GitHub の Web インターフェースで Merge Pull Request ボタンを押すとコミットの AUTHOR_EMAIL がこれになる。
たぶんメールアドレスを非公開にしているとこれになるような気がする。悩ましい。
sudo: false # Use container-based environment language: perl perl: - "5.12" - "5.14" - "5.16" - "5.18" - "5.20" cache: directories: - local # Caches installed modules before_install: - which carton || cpanm --notest Carton - carton version install: - carton install script: - make -f ci.mk cover after_success: - make -f ci.mk coveralls
local/
) をキャッシュに含める……をやっている。
テストで実行するコマンドを Makefile にまとめる。
CARTON_EXEC = carton exec -- COVER_IGNORE_RE = ^t/|^local/ test: $(CARTON_EXEC) prove t/ cover: $(CARTON_EXEC) cover -test -ignore_re '$(COVER_IGNORE_RE)' -make 'make -f ci.mk test' coveralls: cover $(CARTON_EXEC) cover -report coveralls
Perl の値 (スカラ値、ハッシュリファレンス、配列リファレンスなど) が JSON のどの型であるかを推論するモジュール。
Perl の 1
という値は JSON の number である、Perl の "a"
は JSON の string である、という風な。
配列やハッシュのようなコンテナ型も再帰的に推論する。[1,2,3]
のような値を与えると array[number] という型である、と推論する。
候補となる型が複数ある場合は直和型 (union type) として報告する。すなわち [1, 'a']
のような値は array[number|string] と推論する。
もともとこのモジュールは JSON schema を自動生成することを目指して作った。
JSON schema は JSON over HTTP API を提供するときに便利な仕様群であるものの、JSON schema それ自体を記述するのはリーズナブルとは言い難い。
これはソフトウェアが再利用することを意図しているので仕方のないことではある。とはいえ書くのはだるい。
ずっと { "type": "object", "properties": {} }
と書いていると気が滅入る。
JSON schema を実際の JSON データを (複数) 与えて自動的に型や構造を出力してくれると便利そうである。
実際、そのようなことをしてくれるライブラリはある。
(参考: JSON Schema Software)
しかしながら当然 (?) Perl のライブラリは紹介されていない。CPAN でよさそうなそれらしいモジュールは見つけられなかった。なので書くことにした。
このモジュールはその一歩です。
JSON::TypeInference を使って実際に与えられたデータから schema を自動生成するスクリプトを書いてみた。
README にあるように eg/
以下の JSON からこんな schema が生成される:
{ "$schema" : "http://json-schema.org/draft-04/schema#", "description" : "TODO", "properties" : { "id" : { "example" : 1, "type" : "number" }, "name" : { "example" : "yuno", "type" : "string" }, "school" : { "properties" : { "location" : { "properties" : { "lat" : { "example" : 14, "type" : "number" }, "lon" : { "example" : 15, "type" : "number" } }, "type" : "object" }, "name" : { "example" : "yamabuki", "type" : "null" } }, "type" : "object" } }, "title" : "TODO", "type" : "object" }
union type の扱いが雑なのは将来的に直したい。
あるオブジェクトのプロパティが必ず出現すべきなのかどうか、も推論できるとよさそうと思って issue を立ててある。
Detect required properties · Issue #5 · aereal/JSON-TypeInference · GitHub
ちょうどいいデフォルト値が見当たらないので、このキーは各環境で適宜、定義せよ、という風にしたい。interface みたいなかんじ。
use strict; use warnings; use Test::More tests => 2; use Test::Fatal qw(exception); { package My::Config::NotImplemented; use strict; use warnings; use Scalar::Util qw(blessed); sub new { my ($class) = @_; return bless {}, $class; } sub equals { my ($self, $other) = @_; return blessed($other) && $other->isa(ref($self)); } }; { package My::Config; use strict; use warnings; use Config::ENV 'MY_ENV'; use constant NOT_IMPLEMENTED => My::Config::NotImplemented->new; sub declare ($) { my ($name) = @_; return ($name => NOT_IMPLEMENTED); } common +{ declare 'log.path', }; config production => { 'log.path' => '/var/log/app.log', }; config test => +{}; }; { package Test::My::Config; use strict; use warnings; use Config::ENV (); sub import { my ($class) = @_; no strict 'refs'; no warnings 'once'; *My::Config::param = \¶m; } sub param { my ($class, $name) = @_; my $value = Config::ENV::param($class, $name); return !My::Config::NOT_IMPLEMENTED->equals($value); } }; Test::My::Config->import; my $envs = do { my $envs = My::Config->_data->{envs}; [ keys %$envs ]; }; for my $env (@$envs) { local $ENV{MY_ENV} = $env; my $current = My::Config->current; for my $key (keys %$current) { ok +My::Config->param($key), "Environment ($env) defines `$key'"; } }
declare($name)
で各環境で定義すべきキーを宣言する。これは対応する値として意味のないオブジェクトを便宜上定義する (Config::ENV は設定値として hashref を期待するので)。
テスト時に Config::ENV を継承したクラスの param($name)
を上書きして、値が NOT_IMPLEMENTED オブジェクトかどうかをアサートする、という風にする。
すると # Failed test 'Environment (test) defines `log.path''
という風にテストが失敗し、たしかに test で log.path が定義されていない、ということがわかる。
JSON schema とか XML とか使うともっとリッチな検証ができそうだけど、Config::ENV でも素朴にできるというコンセプト。
トークしつつ、トークを聞いたりなどした。
いろいろ聞いたけど印象的だったトークについて。
kazuho さんの発表を初めて見た。声が高い。
僕は大学を中退していてアカデミアどころか学部の卒業論文がどういう雰囲気なのかよく知らないけれども、論文やベンチマークなど根拠となるデータが必ずあってそれを軸に話がなされるので、けっこう速いペースにも関わらずすんなり聞くことができた。
アセットコンパイルを行う動機はリクエスト数を減らすだけではないので必ずしも HTTP/2 によってアセットコンパイルという考え方が無用の長物になることはないだろうと思った *1。とはいえ、言及されたように考え方を変える必要はあるだろうと思う。
言ってしまえば高々60分に満たない時間で HTTP/2 について一本筋の通った理解が得られたのは、事前に断片的な知識が自分にあったことを差し引いてもすごいことだな、と思った。
また、発表の内容自体もさることながら、自分の中で「わかりやすい」「聞きやすい」プレゼンテーションの具体例がひとつできたのも尊い。
WebAudio と信号入門の話。
「今まで Web エンジニアはディスプレイという限られた光情報しか操ることしかできなかったが、WebAudio によって空気を振動させディスプレイを越えて影響を及ぼすことができる」という風なことをおっしゃっていた。
少しおどけた風だったような気がするけれども、実際、胸を打たれた。僕はわりと音楽が好きで音が出ると喜ぶので WebAudio が Web API に入るときはおもしろそうだ、と思ったけれどもそれ以上のことは考えられなかった。
音波とか音響に関する知識はわずかながらあったけれども、信号の話は不明だったのでとてもおもしろかった。とてもエンターテイナーだなあ、と感じた。
タイトルにある通り Go におけるプロファイリングおよび最適化の話で、それはそれでもちろん参考になったけれども、思いがけずライブコーディングを見ることができてとてもよかった。
2012年にチケットを買っていて行くつもりだったけれどもぎりぎりでやめて、別の人に譲った。2014年も参加しなかった。どちらの年もトークを応募しなかったから。
自分はエンジニアで普段からインターネットなどを通じて情報を得たり OSS コミュニティからソフトウェア資産を享受し (つつ、わずかながら還元し) ており、では YAPC のようなカンファレンスで自分が果たせる役目とはなんなのかと問うてみると、おもしろトークをすることだと思っている。
また、そうやってトークをするという形で還元することをイベントに参加する条件として課しているのは「お客様気分」を持たせないためでもある。YAPC は有料のイベントなのでなおさらである。
YAPC::Asia Tokyo にて2回スピーカーとして登壇できたことはとても褒まれ高いことだと思っている一方で、YAPC::Asia Tokyo に区切りがついたとしてもそういったコミュニティへの還元への考え方は変わることはないだろう。
おもしろトークのために技術を磨いていかねば。
みなさまお疲れさまでした。
*1: たとえば browserify などはブラウザにおける JS 実行環境においてモジュールシステムがないことに対するアプローチである
世界展開する大規模ウェブサービスのデプロイを支える技術 - YAPC::Asia Tokyo 2015
2年ぶり2度目の YAPC::Asia Tokyo でタイトルにある通り発表をしてきました。資料は近日公開予定です。
立ち見続出のようで満員御礼といった具合で非常に嬉しいしありがたいことです *1。
懇親会などで「おもしろかった」と声をかけてもらえることが多くて非常に嬉しかったです。 「おもしろかった」「最高」と思ったらぜひ ベストトークに投票 してもらえると嬉しいです!!!
*1: もっとも立ち見されていた方は60分近いトークだったので大変だったでしょうが
YAPC::Asia Tokyo 2015 も来週に控えて、どういうトークを見にいくか悩むこの頃なのでカレンダーをつくりました。
トークリストから素朴にスクレイピングして作ってあります。organizer にスピーカーの名前が入れられるとよかったのですが、メールアドレスが必須で手頃に入手できそうになかったので諦めてイベントのサマリに入れてあります。
Google Calendar などにインポートするなどして、どうぞご利用ください。
gulp とかを使っているプロジェクトの場合、ビルドツール類も devDependencies
に含めてバージョンを固定したいという要求があると思う。
ところが実行ファイルにパスを手軽に通したいという理由のみで npm install -g gulp
などしてしまうとバージョンが固定できなくなってしまい、本末転倒である。
とはいえ ./node_modules/.bin/gulp build
してくれ、というのも面倒であるので、どうするとよいのか書いておく。
たとえば direnv を使う。
# .envrc export PATH="$(npm bin):$PATH"
direnv allow
.envrc は作業ディレクトリを移動した時に評価されるので、npm bin
の出力が異なる環境でもそれぞれうまく動く (はず) なのでリポジトリに含めてもよい。
package.json の scripts
フィールドに定義したタスクは npm run NAME
で実行できる。
また、この scripts
フィールドに定義したタスクの実行時には node_modules/.bin
にパスを通した上で実行される。
In addition to the shell's pre-existing PATH, npm run adds node_modules/.bin to the PATH provided to scripts.
run-script | npm Documentation
統一されたインターフェースを用意するという意味でも scripts
フィールドは望ましい。
direnv と scripts フィールドは役割が少し異なるので、どちらか一方のみを利用するということもなく両方活用できるとよさそうですね。
スコアメーカーという楽譜入力ソフトがあってこれを買ってもらって遊びはじめたところ、MIDI というもので勝手にコンピュータに演奏させることができるとわかって大喜びしはじめた。
ひたすら入力して演奏させる。アーティキュレーションが思ったかんじではないので調整してまた聞く、の繰り返しを土日のあいだ飽きることもなくやっていた。
小学生を卒業したころだったかもしれないけど、ヤマハがミッドラジオプレイヤーというソフトを公開した。
ミッドラジオプレイヤーはソフトウェア音源を内蔵していて、スコアメーカーに再生させるよりずっとリッチな音色になるし、なによりそれが無料で使えるのでヤマハ最高! と唱えながら毎日使っていた。
その後に MP3 というものの存在を知ることとなる。MP3 はダウンロードに時間がかかるけどなにやらきれいな音で MIDI が聞ける、という理解のものだった。
後に MP3 は音声を録音するためのフォーマットのことであり、自分が言っていた MP3 はいいソフトウェア音源の演奏を録音したものだと知る。わからなかったことがわかってよかった、と思う一方で、自分が MP3 を作れたとしてもソフトウェア音源を買わなければいけないことを知ってがっかりした。
しかし自分にはミッドラジオプレイヤーがあった。ミッドラジオプレイヤーで再生してそれを録音すればいいということを思い付いたときは自分が天才かと思った。
作った曲はヤマハの音楽投稿サービス (もうなくなった) に投稿していた。
打ち込みしてるうちにいい曲ができたので、中学生にあがってから朝日作曲賞に応募したことがあった。
打ち込みしていたのは悪夢っぽくはないけど、できた曲のことを思い出すと悪夢っぽい。
これに似た問題。
tmux のセッション内だったので試しに reattach-to-user-namespace electron ...
としたらうまくいった。