iTerm を全画面で表示するのをやめて画面の左半分に表示するようにした

Cmd-Return で全画面にできるのでそのようにしていたけれども、実際のところターミナルが全画面で表示できていて嬉しいことってあまりなかったので左半分に出すようにした。

f:id:aereal:20151221145104p:plain

上半分ではなく左半分にしているのは、ターミナルで見たい情報は幅よりも高さがあることが多いから *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回まで抑えられます。

accumulate-call で簡潔に書く

バッファリングの詳細から離れて、アプリケーションコードを書く側から見ると「T[] を受け取る関数」を「T を受け取る関数」に変換している、といえます。

そこでこの素朴な関数の変換のみを行う accumulate-call という NPM モジュールを公開しました。

github.com

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回だけ送信ボタンを押してください)

hatenacorp.jp

明日は id:hakobe932 です。

*1: サービス開発合宿 - Hatena Developer Blog

*2:私は連打したくなります。

*3: 特にことわりがない限り、「イベント」は DOM Event を、「リソース」は REST アーキテクチャスタイルにおける文脈のリソースを指すものとします。

JSON::TypeInference 1.0 と JSON::Schema::Generator (trial) をリリースした

JSON::TypeInference 1.0

metacpan.org

だいたい欲しいものは実装したので覚悟を決めるという意味での 1.0 リリース。

これはなにか?

Perl の値 (スカラ値、ハッシュリファレンス、配列リファレンスなど) が JSON のどの型であるかを推論するモジュール。

Perl の値から JSON の型を推論するモジュールを書いた - Sexually Knowing

できること

後述の JSON::Schema::Generator のような使い方を念頭に置いてある、というか、そのために作った。

JSON::Schema::Generator

metacpan.org

与えられたデータから型を推論し、構造を再帰的に辿りながら、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 から外すだけでいいのか、もうちょっとユースケースを見極めたい。

JSON::TypeInference 0.04 で null かもしれない値を Maybe 型として推論するようにした

metacpan.org

github.com

Null と値型のただ2つからなる Union 型を Maybe という特別な型として推論するようにした。ちょっと便利。

Maybe だったら {"type" : ["null", "string"]} のように出力するようにすると、より厳密な JSON スキーマを生成することができるようになった。

github.com

ほか JSON::TypeInference::Type::* にドキュメントを追加するなどしました。

どうぞご利用ください。

最近の CPAN モジュールを作るときの構成

github.com

Minilla を使っている。minil new してからちょっと手を入れる。

.mailmap

<aereal@aereal.org> <aereal@users.noreply.github.com>

GitHub の Web インターフェースで Merge Pull Request ボタンを押すとコミットの AUTHOR_EMAIL がこれになる。

たぶんメールアドレスを非公開にしているとこれになるような気がする。悩ましい。

.travis.yml

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

……をやっている。

ci.mk

テストで実行するコマンドを 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

感想

Dist::Maker を使ってみようかとおもったけど依存モジュールが多くてちょっとためらう。

Perl を書きはじめて4年くらいになるけど、小さな CPAN モジュールをリリースするようになってからなんとなく Perl を書くのが好きになってきた気がする。

Perl の値から JSON の型を推論するモジュールを書いた

github.com
metacpan.org

これは何か

Perl の値 (スカラ値、ハッシュリファレンス、配列リファレンスなど) が JSON のどの型であるかを推論するモジュール。

Perl1 という値は 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 schema を自動生成するデモ

github.com

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

感想

型推論などというのもおこがましいほど素朴な実装だけど、しゅっと書けたわりには便利なものができたと思う。

どうぞご利用ください。

Config::ENV で各環境で必ず定義すべきキーを定めておく

metacpan.org

ちょうどいいデフォルト値が見当たらないので、このキーは各環境で適宜、定義せよ、という風にしたい。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 = \&param;
  }

  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 でも素朴にできるというコンセプト。

#yapcasia に参加した

トークしつつ、トークを聞いたりなどした。

1日目

いろいろ聞いたけど印象的だったトークについて。

yapcasia.org

kazuho さんの発表を初めて見た。声が高い。

僕は大学を中退していてアカデミアどころか学部の卒業論文がどういう雰囲気なのかよく知らないけれども、論文やベンチマークなど根拠となるデータが必ずあってそれを軸に話がなされるので、けっこう速いペースにも関わらずすんなり聞くことができた。

アセットコンパイルを行う動機はリクエスト数を減らすだけではないので必ずしも HTTP/2 によってアセットコンパイルという考え方が無用の長物になることはないだろうと思った *1。とはいえ、言及されたように考え方を変える必要はあるだろうと思う。

言ってしまえば高々60分に満たない時間で HTTP/2 について一本筋の通った理解が得られたのは、事前に断片的な知識が自分にあったことを差し引いてもすごいことだな、と思った。

また、発表の内容自体もさることながら、自分の中で「わかりやすい」「聞きやすい」プレゼンテーションの具体例がひとつできたのも尊い。

yapcasia.org

WebAudio と信号入門の話。

「今まで Web エンジニアはディスプレイという限られた光情報しか操ることしかできなかったが、WebAudio によって空気を振動させディスプレイを越えて影響を及ぼすことができる」という風なことをおっしゃっていた。

少しおどけた風だったような気がするけれども、実際、胸を打たれた。僕はわりと音楽が好きで音が出ると喜ぶので WebAudio が Web API に入るときはおもしろそうだ、と思ったけれどもそれ以上のことは考えられなかった。

音波とか音響に関する知識はわずかながらあったけれども、信号の話は不明だったのでとてもおもしろかった。とてもエンターテイナーだなあ、と感じた。

yapcasia.org

2日目

yapcasia.org

タイトルにある通り Go におけるプロファイリングおよび最適化の話で、それはそれでもちろん参考になったけれども、思いがけずライブコーディングを見ることができてとてもよかった。

ごく個人的な感想

2012年にチケットを買っていて行くつもりだったけれどもぎりぎりでやめて、別の人に譲った。2014年も参加しなかった。どちらの年もトークを応募しなかったから。

自分はエンジニアで普段からインターネットなどを通じて情報を得たり OSS コミュニティからソフトウェア資産を享受し (つつ、わずかながら還元し) ており、では YAPC のようなカンファレンスで自分が果たせる役目とはなんなのかと問うてみると、おもしろトークをすることだと思っている。

また、そうやってトークをするという形で還元することをイベントに参加する条件として課しているのは「お客様気分」を持たせないためでもある。YAPC は有料のイベントなのでなおさらである。

YAPC::Asia Tokyo にて2回スピーカーとして登壇できたことはとても褒まれ高いことだと思っている一方で、YAPC::Asia Tokyo に区切りがついたとしてもそういったコミュニティへの還元への考え方は変わることはないだろう。

おもしろトークのために技術を磨いていかねば。

みなさまお疲れさまでした。

*1: たとえば browserify などはブラウザにおける JS 実行環境においてモジュールシステムがないことに対するアプローチである

#yapcasia で『世界展開する大規模ウェブサービスのデプロイを支える技術』という発表をした

世界展開する大規模ウェブサービスのデプロイを支える技術 - YAPC::Asia Tokyo 2015

2年ぶり2度目の YAPC::Asia Tokyo でタイトルにある通り発表をしてきました。資料は近日公開予定です。

立ち見続出のようで満員御礼といった具合で非常に嬉しいしありがたいことです *1

懇親会などで「おもしろかった」と声をかけてもらえることが多くて非常に嬉しかったです。 「おもしろかった」「最高」と思ったらぜひ ベストトークに投票 してもらえると嬉しいです!!!

*1: もっとも立ち見されていた方は60分近いトークだったので大変だったでしょうが

YAPC::Asia Tokyo 2015 のトークスケジュールを集めたカレンダーを作った

https://www.google.com/calendar/ical/037qm3orvt1bkf5g2lsi1br0bg%40group.calendar.google.com/public/basic.ics

YAPC::Asia Tokyo 2015 も来週に控えて、どういうトークを見にいくか悩むこの頃なのでカレンダーをつくりました。

yapcasia.org

トークリストから素朴にスクレイピングして作ってあります。organizer にスピーカーの名前が入れられるとよかったのですが、メールアドレスが必須で手頃に入手できそうになかったので諦めてイベントのサマリに入れてあります。

Google Calendar などにインポートするなどして、どうぞご利用ください。

npm install -g gulp とかしない流儀

gulp とかを使っているプロジェクトの場合、ビルドツール類も devDependencies に含めてバージョンを固定したいという要求があると思う。

ところが実行ファイルにパスを手軽に通したいという理由のみで npm install -g gulp などしてしまうとバージョンが固定できなくなってしまい、本末転倒である。

とはいえ ./node_modules/.bin/gulp build してくれ、というのも面倒であるので、どうするとよいのか書いておく。

node_modules/.bin に自動でパスを通す

たとえば direnv を使う。

# .envrc
export PATH="$(npm bin):$PATH"
direnv allow

.envrc は作業ディレクトリを移動した時に評価されるので、npm bin の出力が異なる環境でもそれぞれうまく動く (はず) なのでリポジトリに含めてもよい。

package.json の scripts フィールドを使う

package.jsonscripts フィールドに定義したタスクは 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 を作れたとしてもソフトウェア音源を買わなければいけないことを知ってがっかりした。

しかし自分にはミッドラジオプレイヤーがあった。ミッドラジオプレイヤーで再生してそれを録音すればいいということを思い付いたときは自分が天才かと思った。

作った曲はヤマハの音楽投稿サービス (もうなくなった) に投稿していた。

www.itmedia.co.jp

打ち込みしてるうちにいい曲ができたので、中学生にあがってから朝日作曲賞に応募したことがあった。

打ち込みしていたのは悪夢っぽくはないけど、できた曲のことを思い出すと悪夢っぽい。

近況

YAPC::Asia Tokyo 2015

yapcasia.org

採択された。

App-xslatert と Text-Xslate-AST-Walker

github.com

App-xslatert というツールを作り始めた。

App-PRT を便利に使っていて、Perl のコードのリファクタリングはだいぶ楽になったので、テンプレートのリファクタリング、たとえば変数名を変えるといったことを同じような使い心地で行いたいと思ったので作り始めた。

github.com

前に作った Text-Xslate-AST-Walker を使っている。AST を変更する機能が必要になったので issue の優先度を上げたりした。

github.com

mackerel-agent で便利に使える OS X 向けプラグインを Homebrew でインストール

github.com

mackerel-agent-plugins-osx というリポジトリを非公式に作って置いておくという活動を前からしている。今のところをバッテリ残量をホストのメトリクスとして投稿できるのみ。

これを新しい MacBook にインストールする上で手軽にしたいと思って、Homebrew の Tap を作った。

github.com

どうぞご利用ください。