開拓馬の厩

いろいろやる

Protobuf を SQS のスキーマ管理に使ってみる

Amazon SQS のスキーマ管理を Protobuf でやってみました。

サンプルコードはこちら。

github.com

.proto ファイル

以下のような .proto ファイルを用意します。

// proto/example.proto

syntax = "proto3";

message Item {
  string product_id = 1;
  float price = 2;
}

message Purchase {
  string user_id = 1;
  repeated Item items = 2;
  // Unix time
  uint32 timestamp = 3;
}

今回は Purchase message を binary serialize した後、 base64 encode したものを SQS に積む方針で進めます。

AWS で SQS のリソースを作る

まずはじめに Amazon SQS のリソースを準備します。 今回は Amazon SQS のキューをひとつ作って、手元の環境からアクセスする方針で進めます(EC2 からアクセスしたりとかバックエンドを Lambda にしたりとかは 面倒 本稿のテーマとは無関係なのでやらない)。 キューをひとつ作るだけなのでマネジメントコンソールをポチポチするだけでもいいのですが、なんとなく Terraform を使ってみます。

予め SQSFullAccess ポリシーを持ったユーザーを作っておき、その Access Key と Secret を使って aws-cli でプロファイルを作成します(AWSのIAMユーザーを作る部分は説明略)。

# Access Key を準備する部分は省略
# aws configure で sqs という profile を作る
$ aws configure --profile sqs
  AWS Access Key ID [None]: XXXXXXXXXXXXXXXXX
  AWS Secret Access Key [None]: XXXXXXXXXXXXXXX
  Default region name [None]: ap-northeast-1
  Default output format [None]: None

# AWS_PROFILE を指定
$ export AWS_PROFILE=sqs

.tf ファイルを準備します。

# terraform/terraform.tf

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_sqs_queue" "queue" {
  name = "example-queue"
}

# Queue の URL を出力する
output "queue_id" {
  value = aws_sqs_queue.queue.id
}

あとは Terraform CLI を叩くだけで、 example-queue というキューが作られて、キューの id が出力されます。 id は URL 形式になっており、キューを積んだり取り出したりする際に必要になります。 後々用いるので環境変数に追加しておきます。

# 初回だけ必要
$ terraform init

# dry run。 実行後に AWS 上にどのような変更が行われるか確認できる
$ terraform plan

# 実際に AWS 上のリソースが作る
$ terraform apply
# =>  queue_id = "https://sqs.ap-northeast-1.amazonaws.com/xxxxxxxxxx/example-queue"

# id を環境変数に登録しておく。ついでに AWS のリージョンも
export QUEUE_URL=https://sqs.ap-northeast-1.amazonaws.com/xxxxxxxxxx/example-queue
export AWS_REGION=ap-northeast-1

Terraformで作成したリソースは terraform destroy で削除できます。使い終わったら忘れずに消しておきましょう*1

Protobuf.js

.proto ファイルと SQS の準備ができたので、やっと実装が始まります。 今回は TypeScriptProtobuf.js を用いて adapter を実装します。

github.com

Protobuf.js はその名の通り Protobuf を取り扱うライブラリです。あくまでも Protobuf を扱うライブラリなので、 gRPC 関連の機能ほぼサポートされていませんが、これはむしろ今回の用途に適っているので好都合。 gRPC の文脈では @grpc/proto-loader と組み合わせて dynamic codegen ((実行時に .proto ファイルを読み込む方式))を行う際によく使われているライブラリです。 しかし、今回は Protobuf.js が提供する CLI ツール pbjspbts を用いて static codegen ((予め .proto ファイルを読み込んでコードを生成し、それを用いる方式)) を行います。

コード生成のコマンドはそこそこ長いので shell script にまとめておきます。

# bin/protogen.sh

#!/usr/bin/env bash
set -eu

NODE_BIN=$(dirname $0)/../node_modules/.bin
PROTO_DIR=$(dirname $0)/../proto/*.proto
OUTPUT_DIR=$(dirname $0)/../generated

rm -f ${OUTPUT_DIR}/*.js ${OUTPUT_DIR}/*.ts
${NODE_BIN}/pbjs --target static-module --out ${OUTPUT_DIR}/index.js ${PROTO_DIR}
${NODE_BIN}/pbts --out ${OUTPUT_DIR}/index.d.ts ${OUTPUT_DIR}/index.js

ちなみに pbts は jsdoc を元に型定義を吐くライブラリ tsd-jsdoc を使っているようです。なので pbjs--no-comment オプションを付けてコメントを省略してしまうと .d.ts ファイルが正しく生成されません(ちょっとしたハマりポイント)。 成功すると以下のような .d.ts ファイルが生成されます。

// generated/index.d.ts から抜粋

import * as $protobuf from "protobufjs";

export interface IItem {
  productId?: string | null;
  price?: number | null;
}
export interface IPurchase {
  userId?: string | null;
  items?: IItem[] | null;
  timestamp?: number | null;
}

export class Purchase implements IPurchase {
  // Message class を作るやつ
  public static create(properties?: IPurchase): Purchase;

  // Message を binary serialize するやつ
  public static encode(
    message: IPurchase,
    writer?: $protobuf.Writer,
  ): $protobuf.Writer;

  // binary を deserialize して Message Class を得るやつ
  public static decode(
    reader: $protobuf.Reader | Uint8Array,
    length?: number,
  ): Purchase;
}

pbts{ hoge?: (T|null); } のような undefined を許容した型を吐きます。これでは少し不便なので undefined を許容しない型へ変換するヘルパーを用意します。

// clinet/types.ts

export type DropUndefined<T> = T extends undefined
  ? never
  : {
      [P in keyof T]-?: T[P] extends undefined ? never : DropUndefined<T[P]>;
    };

// DropUndefined<{ hoge?: string|null }> = { hoge: string|null }

DropUndefined<T> を用いることで、値無しの項目について明示的に null を付ける必要が出るため、意図しない代入漏れを防ぐことができます*2

メッセージを積むやつ(client)を書く

こんな感じ

// client/index.ts

import { IPurchase, Purchase } from "../generated";
import { SQS } from "aws-sdk";
import { DropUndefined } from "./types";

async function main(): Promise<void> {
  const QueueUrl = process.env.QUEUE_URL;
  if (QueueUrl === undefined) {
    console.error("Please set env `QUEUE_URL`");
    process.exit(1);
  }
  const region = process.env.AWS_REGION ?? "ap-northeast-1";

  // TS の object
  const rawMessage: DropUndefined<IPurchase> = {
    userId: "hoge",
    items: [{ productId: "fuga", price: 123.4 }],
    timestamp: new Date(0).valueOf(),
  };

  // Protobuf.js が生成した Class
  const message: Purchase = Purchase.create(rawMessage);

  // serialize されたバイト列
  const serialized: Uint8Array = Purchase.encode(message).finish();

  // バイト列を base64 でエンコードして文字列にしたもの
  const base64String: string = Buffer.from(serialized).toString("base64");

  // SQS に積む処理
  await new SQS({ region })
    .sendMessage({
      MessageBody: base64String,
      QueueUrl,
    })
    .promise();
}

main()
  .catch(e => {
    console.error(e);
    process.exit(1);
  })
  .then(() => {
    console.log("finished");
    process.exit(0);
  });

実行するとキューにメッセージが積まれます。マネジメントコンソールや aws-cli から積まれているメッセージ数を確認すると値が増えていることがわかります。

$ yarn ts-node client

# finished

$ aws sqs get-queue-attributes --queue-url $QUEUE_URL --attribute-names ApproximateNumberOfMessages

# {
#    "Attributes": {
#        "ApproximateNumberOfMessages": "1"
#    }
#}

キューを処理するやつ(consumer)を書く

キューを処理するコードはこんな感じになります

// consumer/index.ts

import { SQS } from "aws-sdk";
import { Purchase } from "../generated";

async function main() {
  const QueueUrl = process.env.QUEUE_URL;
  if (QueueUrl === undefined) {
    console.error("Please set env `QUEUE_URL`");
    process.exit(1);
  }
  const region = process.env.AWS_REGION ?? "ap-northeast-1";

  const sqsInstance = new SQS({
    region,
  });

  // SQS からメッセージを取り出す
  const sqsResult = await sqsInstance
    .receiveMessage({
      QueueUrl,
    })
    .promise();
  if (sqsResult.Messages === undefined || sqsResult.Messages.length === 0) {
    console.log("no messages found");
    process.exit(0);
  }

  // base64 string から Protobuf.js の Message Class を生成する
  const getMessage = (base64String: string): Purchase => {
    // Protobuf の仕様に従って serialize されたバイト列
    const serialized = Uint8Array.from(Buffer.from(base64String, "base64"));
    // Protobuf.js の Message Class
    return Purchase.decode(serialized);
  };
  const messages = sqsResult.Messages.map(m => m.Body)
    .filter(<T>(x: T | undefined): x is T => x !== undefined)
    .map(str => getMessage(str));

  // 各 message について、何らかの処理を行う
  // 今回は console.log に適当に文を吐くだけ
  messages.forEach(message => {
    // `message.userId` 等でプロパティにアクセスできる
    const userId = message.userId;
    // 型も付く Protobuf で `uint32 timestamp = 3;` と定義したので、`message.timestamp` は number 型
    const date = new Date(message.timestamp);

    // ただし、 Message をネストすると型が正しく付かない
    // message.items[n].price は `number | null | undefined`
    const sumPrice = message.items.reduce((sum, i) => sum + (i.price ?? 0), 0);

    console.log(
      `ユーザー(${userId})が ${date.toLocaleDateString()} に合計 ${Math.floor(
        sumPrice,
      )}円の買い物をしました`,
    );
  });

  // 取得したメッセージを消す
  await sqsInstance
    .deleteMessageBatch({
      QueueUrl,
      Entries: sqsResult.Messages.map(m => ({
        Id: m.MessageId!,
        ReceiptHandle: m.ReceiptHandle!,
      })),
    })
    .promise();
}

main()
  .catch(e => {
    console.error(e);
    process.exit(1);
  })
  .then(() => {
    console.log("finished");
    process.exit(0);
  });
$ yarn ts-node consumer

# ユーザー(hoge)が 1970/1/1 に合計 123円の買い物をしました
# finished

こんな感じで base64 string から値を復元できます。 少し気になったのは Purchase.Items の型が IItems[] になっている点でしょうか。 IItems{ productId?: (string|null); price?: (number|null); } です。 本来なら値が指定されていない項目については空文字列や 0 で初期化されているため、 { productId: string; price: number; } とみなしてよいのですが、 null や undefined を含む型として扱われてしまい不便です。 as で対処する等の工夫が要るかもしれません*3

余談:serialize について

今回は Protobuf のメッセージをバイナリに serialize しました。 ですが、 Protobuf はバイナリ以外のフォーマットにも serialize 可能です。 例えば、 JSON に serialize すれば、base64 エンコードせずとも SQS に乗せることができますし、デバッグ目的で手動ポーリングする際に可読性のあるメッセージが拾えて便利です。 送受信双方のライブラリが対応しているのなら JSON を用いる選択肢も検討の余地がありそうです*4

まとめ

いかがでしたか?(枕詞) 本記事では Protobuf を SQS のスキーマ管理に用いる例を紹介しました。 今回は client/consumer ともに TypeScript で実装しましたが、 Protobuf は language-neutral をウリにしているので、別の言語で書いたアプリケーションを使うこともできます。 node 製の BFF サーバーがメッセージを積んで、 Java や Go や Rust や Haskell で書いた job worker が処理するといった運用は実際に使う場面がありそうです。

*1:Amazon SQS は 100 万リクエスト/月 まで無料でキューの維持費もとくにないので本記事の内容を一通り試した後にキューを放置してもとくに課金されることは無い。何らかの手違いで悪意ある第三者に id が見つかってイタズラされた場合はその限りではないが

*2:oneof が使われている proto との相性はあまりよくないので万能では無いが

*3:このあたりの型定義の緩さは JavaScript を使う上で避けられない面もある。現状 TS の型定義がちゃんとしている Protobuf ライブラリが見当たらず、 client 側はまだしも consumer 側を JS/TS で書くのは面倒そうだという印象を覚えた。 Protobuf は多言語対応なので、 consumer 側については別の言語で書いてしまうほうがいいのかもしれない

*4:Protobuf.js は JSON への serialize/deseriualize 双方に対応しているようなので、少し書き換えれば JSON を積む実装に変更できる

社内勉強会はじめました

弊社で社内勉強会をした話を書く

弊社の紹介

本ブログで現在勤務している会社に言及したことがあまり無いので改めて紹介をします。 私は昨年の9月から株式会社HERPでエンジニアをしています。 HERPは採用管理システム(Applicant Tracking System; ATS)を提供している会社です。 主にTypeScriptとHaskellを使ってプロダクト開発をしています。 弊社にはエンジニアが十数名*1在籍しています。 中にはブラウザのレンダリングエンジンにContributeしている人がいたり、計算機科学の研究をしている現役大学院生がいたりします。メンバーの知識に差があるのでそれを埋めるためにも勉強会をやりたいなあという声が社内であがっていました。

また、弊社では「スクラム採用」という採用活動を推進しています。 スクラム採用とは採用施策を人事担当者ではなく現場社員が立案・遂行するという採用活動です。 エンジニアの採用はエンジニアが、デザイナーの採用はデザイナーが介入するといった感じです。 エンジニアがスクラム採用をやっていくにはTwitterやブログでテクノロジーに関する話をしたり、イベントで登壇したりして注目を集める能力があるとうれしい。 社内で勉強会を行うのは知識を発信する訓練として良さそうだというのも社内勉強会を開催した動機のひとつです。

社内勉強会の様子

8月1日に私が企画して社内勉強会を行いました。 1回目なのでひとまず30分ぐらいで終わるように発表者を2名集めて発表7分、質疑応答7分としました。

Cybaiさん:今年の6月にMozilla Whistler All Hands 2019に参加した話をしてもらいました。 hiroqnさん:nixパッケージマネージャーの話をしてもらいました。

主催者としての反省

この手のイベントの企画は初めてだったのですが、やってみた反省としては以下のとおりです。

  • 発表準備のケツ叩きは必要
  • タイムキーパーは必要
  • 発表者は案外集めづらい

あと、今後も社内勉強会を定期的に開催するようにしたいと思っています。 ゆくゆくは社外の人も参加できる形にしていきたいと考えており、他社がどのようにして勉強会を企画・開催しているのかとか、エンジニア勉強会をやる文化を根付かせるためにどんな試みをしているのかとかを調査しています(有識者の方がいればコメントくださると幸いです)。

Scrapbox使うとよさげ?

発表スライドをScrapboxのプレゼンテーションモードで作ると手軽でいいんじゃないかなと思います。実際にhiroqnさんはスライドをScrapboxで作っていました。

f:id:pf_siedler:20190815142240p:plain
Scrapboxのプレゼンテーションモード。「あなたとJAVA,今すぐダウンロード」っぽい改行が入るのはご愛嬌

パブリックプロジェクトで作ればそのまま公開もできます。今回の発表資料も↓で公開しています。

このScrapbox projectですが、スライド以外にも弊社で使っているフレームワークや技術の解説もまとめています。題してHERP-Technoteです。 今年6月頃からすこしづつ書き始めて現在50以上のページができています。 今後も記事を増やしていく予定なので読んでくださると嬉しいです。

scrapbox.io

まとめ

  • 勉強会やっていきたい
  • Technoteもちょくちょく更新されていくので良かったら見てください

*1:学生インターンと業務委託を含む

FuseBoxというイケてるmodule bundlerがあるらしい

FuseBoxというmodule bundlerがイケてるらしいので使ってみた github.com

どこらへんがイケてるのか

公式サイトGitHubのREADMEによると以下のような利点があるらしい。

  • 早い
  • デフォルトでTypeScriptやJSX記法に対応している
  • devサーバー内蔵
  • HMR(hot module replacement)がサポートされている
  • configが楽

実際に使った感想としては、configファイルが簡素な点、で専用のCLIを使わない点、後述のWebIndexPluginなどが主な魅力だと思われる。

実際に使ってみた

実際に使ったソースコードこちら

以降は、実際に書いたコードを提示しながら*1、FuseBoxの使い方を説明する。 「ただbuildするだけ→devサーバーを使う→pluginを入れる」のように後半になるにつれて高度な機能の説明になっていく。

とりあえずbuild

まず適当なtsファイルをbuildしてみる。src/ディレクトレリを作って、その中にindex.tsという名前で以下のソースコードを保存する。

// index.ts

const msg: string = "Hello World";
console.log(msg);

次にyarnを初期化して、fuse-boxtypescriptをインストールする。

$ yarn init
$ yarn add -D fuse-box typescript

READMEに習って以下のようなconfigファイルを書き、fuse.jsの名前で保存する。

// fuse.js
const { FuseBox } = require("fuse-box");

const fuse = FuseBox.init({
  homeDir: "src",
  output: "dist/$name.js",
  sourceMaps: true,
});

fuse.bundle("app").instructions("> index.ts");

fuse.run();

fuse.bundle()ソースコードを一つのjsファイルにまとめるコマンドで あり、.instructions()でビルドの起点になるファイルを指定する(webpack.config.jsでいうところのentry)。

設定ファイルを記述したら、以下のコマンドを実行する。FuseBoxはwebpack-cliの専用のCLIコマンドは用意されておらず、configファイルをそのまま実行する形式を取っている(npm i -gしたくない派の人喜びそう)。

node fuse

すると、dist/ディレクトリが作られ、中にビルド結果がapp.jsapp.js.mapの名前で出力される。

ちなみに今回のように、tsconfig.jsonを作らずに実行すると、src/以下に自動生成してくれる。 targetbaseUrlなどの設定項目はfuse.jsに書いた通りにしてくれるのでちょっと便利。

devサーバーを動かす

fuse.jsにdevサーバーを立てる設定を追記する。 ついでに出力されるjsとsource mapをブラウザ向けのものにしておく。

// fuse.js
const { FuseBox } = require("fuse-box");

const fuse = FuseBox.init({
  homeDir: "src",
  output: "dist/$name.js",
  target: "browser@es5", // ブラウザ向けのコードを吐かせる。ついでにバージョンもes5に指定
  sourceMaps: {vender: true}, // ブラウザ向けのsourceMapを出すようにしておく
});

// devサーバーの設定
fuse.dev({
    port: 8888 // port番号。デフォルトは4444。
    root: 'dist' // devサーバーのルートディレクトリ。デフォルトはFuseBox.init()の`output`で指定したディレクトリ。
});

fuse.bundle("app")
    .instructions(`> index.ts`)
    .watch() // tsを書き換えるたびに差分コンパイルを行う
    .hmr(); // hot module replacementを有効にする

fuse.run();

ES5で出力するようにしたので、index.tsもES6以降の機能を使ったやつにしてみる。

enum LogLevel {
    Info,
    Error,
}
type Message = { type: LogLevel; message: string };

const showMessage = (m: Message): string => {
    switch (m.type) {
        case LogLevel.Info:
            return `📢 ${m.message}`;
        case LogLevel.Error:
            return `⚠️ ${m.message}`;
        default:
            throw new Error("invalid message");
    }
};

const hello: Message = { type: LogLevel.Info, message: "Hello World" };
document.querySelector("body").innerHTML = showMessage(hello);

最後にdist/index.htmlを作成すれば準備完了。

<!-- index.html -->

<body>
  <div id="app"></div>
  <script src="/app.js"></script>
</body>

node fuseを実行し、localhost:8888にアクセスすると「📢 Hello World」が表示される。 ついでにapp.jsを確認すると、ES6やTypeScript特有の記法がES5に変換されていることが確認できる。

// app.js の抜粋
var LogLevel; // TypeScriptのenumで書いた箇所
(function (LogLevel) {
    LogLevel[LogLevel["Info"] = 0] = "Info";
    LogLevel[LogLevel["Error"] = 1] = "Error";
})(LogLevel || (LogLevel = {}));
var showMessage = function (m) { // ES6のarrow関数で書いた箇所
    switch (m.type) {
        case LogLevel.Info:
            return "\uD83D\uDCE2 " + m.message;

WebIndexPluginでindex.htmlを自動生成

前章ではindex.htmlを手動でdist/に置いた。しかし、dist/は自動出力専用にして、手書きのコードは他のディレクトリで管理したいという要求もあるだろうと思われる(私はそうしたい)。 そこで、WebIndexPluginというものを使ってそれを実現する。

まず、src/public/template.htmlを作る。

<!-- hemplate.html(Emmetのテンプレをちょっと書き換えただけ) -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>My Page</title>
</head>
$css<!-- ここにstyleタグが挿入される -->
<body>
  <div id="app"></div>
  $bundles <!-- ここにscriptタグが挿入される。設定次第で、src=""で別ファイルから読み込むかinlineか選べる -->
</body>
</html>

fuse.jsプラグインの設定を追加する。

// fuse.js

const { FuseBox, WebIndexPlugin } = require("fuse-box"); // WebIndexPluginもimportする

const fuse = FuseBox.init({
  homeDir: "src",
  output: "dist/$name.js",
  target: "browser@es5",
  sourceMaps: {vender: true},
  plugins: [WebIndexPlugin({ template: "src/public/template.html" })], // これを書き足す
});

実行するとdist/index.htmlが自動生成される。ちなみに、WebIndexPluinの引数でtemplateを指定しない場合、適当なhtmlファイルを自動生成が作られる。便利。

まだまだあるぞ

まだ色々紹介したい機能があるが、長くなったので記事を分けたい。 残っているネタは以下の通り。

  • AltCSSを使ってみた
  • JSXを使ってみた
  • fuse-box/sparkyで静的ファイルのコピーをしてみた

ついでにwebpackあたりとの比較もやってみたい(未着手)。

ただ、7月17日時点で、依存ライブラリに脆弱性が見つかっているようなので本格的に使うなら次のバージョンを待った方がよさそう*2f:id:pf_siedler:20190717180724p:plain

*1:清書の段階で修正した箇所もあるためgitのlogと不整合な箇所もある

*2:version 4の準備が進んでいるらしく、hot fixするのかv 4公開のタイミングで直すのかよくわからない。7月17日時点でdependencyのバージョンを上げるissue/PRは出てないのでcontributionチャンスかも?

弊社でCycle.jsに寄付した話

Open Collectiveを使って業務で使っているOSSライブラリに寄付をした話をする。

Cycle.js

会社でCycle.jsというフロントエンドフレームワークを使っている。

github.com

Cycle.jsはステート管理やをひとつの関数として実装して、 ブラウザ上の操作やHTTP通信などの"外界"の動作を関数外の処理に任せるというアーキテクチャを採用している。

f:id:pf_siedler:20190712162458p:plain
Webアプリはブラウザ操作やHTTPレスポンスを受け取り、ビューやHTTPリクエストを吐く関数である

弊社プロダクトのフロントエンド部分はほとんどCycle.jsで実装しており、大変お世話になっている。 そこで、Cycle.jsのOSSコミュニティに資金援助をしよういう話が社内であがった。

Open Collective

Cycle.jsはOpen Collectiveというサイトで寄付金を受け付けている。

opencollective.com

Open Collectiveは投げ銭サービスの一種で、主にGitHubOSSコミュニティ向けに作られている。

寄付する側での会員登録、寄付の方法を説明する。

会員登録

個人で会員登録をする場合、メールアドレスと名前を入力するだけで登録できる。 パスワード入力は不要で、登録メアド宛に送られてくるmagic linkを開くことでログインする。*1

法人として登録する場合、上記に追加して

  • 団体名
  • WEBサイトURL
  • GitHub Organizationの URL(任意)
  • Twitter ID(任意)

の入力を求められる。 GitHub OrganizationやTwitter IDを入力しておくと、団体のアイコンを自動で取得してくれるみたいなのでちょっと便利。

お支払い

次に寄付の方法を説明する。

まず寄付先のコミュニティのサイトを開き、「コントリビュート」のボタンをクリックする。

すると、コミュニティが指定した寄付のプランが表示される。 ここでプラン通りに寄付をしても良いが、ページ下部の「Or make a custom financial contribution」を選んで自分で寄付額を決めることもできる。

f:id:pf_siedler:20190712163138p:plain

自分で寄付額を決める場合、金額と寄付の頻度(1回、毎月、毎年の3パターン)を入力する。

f:id:pf_siedler:20190712163314p:plain
ちなみに通貨単位がユーロ建てになっているコミュニティもある。寄付金を決める際は為替を要チェック

寄付金の支払い方法は以下から選べる。

  • クレジットカード決済
  • PayPal*2
  • 銀行振込

銀行振込で海外送金するのは色々大変そうなので、実質クレカとPayPalの二択になると思われる。

実際に寄付してみた

社長にお願いしたら1000ドルぽんと出してくれた :sasuga_shacho:

そして公式サイトに載った。

f:id:pf_siedler:20190712163708p:plain

さいごに

Cycle.jsはかなりイケてるフレームワークなので、ガンガン開発してほしいですね。

herp.careers

herp.careers

*1:つまり、メールを受け取れる人が本人という形で本人確認を行っている。

*2:寄付先の団体が米ドル建てで寄付を受け付けている場合のみ?

開拓馬 バージョン 24.0.0を公開しました

24歳の誕生日なので去年からの差分をリリースノートっぽく書いた

  • 自力で生活費を稼ぐ機能を追加しました
  • TypeScriptを扱う機能を追加しました
    • Cycle.jsでフロントエンドを実装する機能を追加しました
    • stylusやpugを使ってある程度の静的サイトを記述する機能を追加しました
    • TypeORMを使ってDBにアクセスする機能を追加しました*1
    • fp-tsio-tsで素のTSより型安全なコードを書く機能を追加しました
    • circleCIのconfigを書く機能を追加しました
    • Jest, Mocha, TypeMoqを使ってテストコードを書く機能を追加しました
    • 稀にDefinitelyTypedにPRを出す機能を追加しました
  • 余剰資産を投資信託で運用するように変更しました from Folio
  • 研究意欲がないのに大学院に行ってしまう不具合を修正しました
  • レーニングジムに週1~2回通いヘルスケアに勤しむ機能を追加しました
  • コーヒーの好みを苦味が強いものから酸味の強いものに変更しました from LIGHT UP COFFEE
  • 認知行動療法を導入することで自己肯定感が急激に低下する不具合を解消しました from デビット・D・バーンズ
    • 何もしていないときに焦りを覚える現象が発生しづらくなりました
    • 根拠の無い自己肯定が有効であるという認識を導入しました
    • 「人間は何を成し遂げたとしても死んで無に帰す」という世界観を持つようになりました from 中島義道*2, ショーペンハウアー, シオラン
    • ↑を「優れた洞察力を持つ」と言い換ることでペシミズムをある種の長所と捉えるようになりました
  • 引っ越しによりロード時間*3を短縮しました
  • 食洗機を利用して時短家事を行うようになりました(めっちゃ便利)
  • 図書館を利用することで読書量を増やしました
    • web予約機能を利用するようになりました(イマドキの図書館は便利ね)
    • 蔵書リクエストを出すようになりました
  • Nintendo Switchで遊ぶようになりました from Nintendo
    • 昨年11月に購入してから400時間ぐらい遊んでる
  • スマホゲームに手を出すようになりました
  • SEKIROクリアしました*5
  • 欲しいものリストで誕生日プレゼントをねだる機能が追加されました

*1:機能を追加してません。昨日TypeORMでmysqlのdockerイメージぶっ壊したので

*2:中島氏の本は14歳の頃に必死で読んでいたが、大人になってまた違う形で解釈できるようになったような気がする

*3:通勤時間の意味

*4:DOTA2 Mod版は操作性がいまいちなのであんまりやってない

*5:一週目不死断ちエンドのみ。50時間ぐらいかかった

研究と鬱と休学の話

これはEEICアドベントカレンダー2日目の記事です。

研究する気のない人が大学院に入って精神状態がボロボロなった話とそこから這い上がった話をします。

TL;DR

  • 人は心が荒むとスピリチュアルに頼りがち。でも認知行動療法に頼ったほうがいい
  • つらくなったら学生相談室にGO
  • 「勉強をしたい」と「研究をしたい」は別。大学院は後者のための場所

研究室と研究生活について

学部4年(去年)の4月から、電子情報工学科のIIS-Lab(矢谷研究室)に配属されました。 その後、大学院に進学した後も在籍し続け、1年半ほどこの研で過ごしました。

IIS-Labの研究内容は公式サイトを見ればわかるので、研究室の雰囲気や学生の過ごし方を軽く説明すると、

  • 毎週水曜日に全体ミーティング、また週一で先生と1対1でミーティング(コアタイムはこれだけ)。
  • 一人1プロジェクト。解決すべき問題を探すところから実験計画まで、全部自分で決める。
  • 研究の進捗管理GitHubのissue、業務連絡はSlack。
  • 論文はOverleafで書く。共有リンクを渡せば、先生も上級生もわりとガッツリ添削してくれる。*1
  • 発表練習が手厚い。卒論発表や輪講の直前はミーティングで発表練習をやる。
  • 学生居室がキレイ。(ベンチャー企業のオフィス感)
  • 修士1年の夏に企業のインターンに行くことが推奨されている。
  • 1/3ぐらいが留学生。*2
  • 計算資源はMicrosoft Azure。研究室でパートナー契約的なものをしてるらしく、高いインスタンスを借りても大丈夫っぽい。(書き忘れていたので2019/01/15追記)

自分で考えたテーマで研究したい人、研究のモチベーションが高い人にはとてもいい環境だと思います。 逆にそうでない人をきっぱりと休学・退学させてくれるのもいいところなのかな……(ごまかしで卒業できてしまうのよりは間違いなく良い)

気の乗らない研究生活。鬱の傾向が現れる

そんな素晴らしい環境に身を置きながらも、研究生活を送るにつれて私の精神はすり減っていきました。

その原因は研究の進捗が芳しくなかったことです。 まず、研究テーマを見つけるという段階から躓きました。 どのような研究をやりたいかというビジョンを明確に持たないまま4年生になってしまったため、やりたい研究が思いつかないという状態に陥りました。 IIS-Labでは、研究テーマは各人が自分で決めるという方針だったため、テーマが降ってくる*3こときも期待できず、ただ途方に暮れる状態でした。 結局、なんとかひねり出した微妙なアイデアを元に研究っぽい何かをでっち上げるというような酷い卒業研究でした。 こんな具合で卒業研究は散々な結果だったのですが、大学院入試には受かっていたためそのまま大学院に進学し、修士の研究でもテーマが見つからないという問題に直面し……

毎週のミーティングで先生に報告できるような進捗を作らなければという重圧や、修論発表の期日までに成果を出せないかもしれないという漠然とした不安と戦う毎日でした。 こうした不安は大学にいる時間だけでなく、買い物している間やご飯を食べている間、寝る前の布団に潜っているときも、趣味の登山の最中ですら、常にこうした不安からは逃れられない状態でした。 自由な時間はたくさんありましたが、心から休める時間は1秒たりともありませんでした。

そんな生活を送っていたら不眠症になり、保健センター精神科*4で診てもらったところ「鬱の傾向*5がある」と言われました。

認知行動療法、マインドフルネス、スピリチュアル

こうして鬱の傾向にあることが発覚したのですが、そこから抜け出すために色々な「あがき」をしました。

まず認知行動療法に取り組みました。 認知行動療法というのは、出来事に対するネガティブな思い込み(自動思考)を言語化し、合理的な思考に修正することで精神を安定させる手法で、鬱やパニック障害の治療方法のひとつです。 精神科医デイビット・D・バーンズの名著「いやな気分よさようなら」や「フィーリングGoodハンドブック*6を読み込みました。 そこに記載されているノートテイキング手法を試したり、日々認知の歪みを意識して生活したりすることで、ネガティブな自動思考を徐々に和らげていきました。 他にも認知行動療法の亜種であるAcceptance Commitment Therapy(ACT)についても調べ、できる範囲で実践しました。

他にもマインドフルネス(瞑想)にも取り組みました。 マインドフルネスとは「何も考えない」状態を作ることでストレスを和らげる手法です。 基本的に道具を使わなくてもマインドフルネスは可能なのですが、ガイドがある方がやりやすいと考えたので、HeadSpacePauseといったアプリを使いました。 ちなみにHeadSpaceのようなMindfulness-based Applicationは学術研究の対象として注目されてきているようです。 コンピュータサイエンスを用いてWell-beingを追求するという目標の元、HCIと精神医学を組み合わせた学際的な研究は最近流行ってきているみたいです(マインドフルネスは決してスピリチュアルやオカルトのたぐいではないぞ)。 「ウェルビーイングの設計論*7」等の書籍に詳しく書いてあるので興味を持った方はご一読してみることをおすすめします。

他にも、南米のスピリチュアルな儀式*8を試したり(今思い返すと相当キテた)、 ちょっとした一人旅に行ったり*9、 半ばヤケになって色々試しました。

認知の歪みを意識すると気分が落ち込みにくくなるという発見は今でも役立ってます。

東大の学生相談ネットワーク

学内の学生相談ネットワークもフル活用しました。 大学生活を送るうちに心を病む人というのはわりと多いらしく、ほとんどの大学には学生相談室という施設が設置されています。 東大の場合、学生相談室の他に、精神保健支援室(保健センター精神科)、コミュニケーション・サポートルーム、なんでも相談室、ピアサポートルームという施設が存在し、まとめて学生相談ネットワークと呼ばれています。 私はピア・サポートルーム以外は全部利用して精神の安定させていきました。 せっかくなので使い方とかどんな雰囲気かというのを書いておきます。

  • 学生相談室
    • スクールカウンセラーの方と相談できる場所
    • 基本的には毎週 or 隔週ぐらいのペースで通院(通室?)する
    • 予約制
      • ネット予約だと1~2週間後になる場合が多い
      • 実は電話予約だと融通が効く場合がある
  • 精神保健支援室(保健センター精神科)
    • 精神科医臨床心理士に診察してもらえる
    • 初回はスクールカウンセラーの方と面談 and アンケート
    • 基本的に薬を処方してくれるだけ。カウンセリングはあまりやらない
    • 診察料は無料、薬代は自費。健康保険が使えず全額負担
    • 初回は窓口に直接行く。二回目以降は予約制
  • コミュニケーション・サポートルーム(コミュサポ)
    • 発達障害の支援 and 自己理解を深めるという位置付けの相談室
    • 基本は学生相談室とほぼ同じ
    • 会話の練習をしたり、知能試験を受けたりすることができる
      • WAIS 3という1回2万円の試験が無料*10
    • 発達障害の医学的な診断を受けることも可能
      • 診断を受ける場合、臨床心理士が両親に電話して子供の頃の様子を聞くというフェイズが挟まる
      • ちなみに私も受けてみたが、発達障害というよりは社交不安症の傾向らしい
    • 隔週ぐらいのペースで通院(通室?)、予約制なのは学生相談室と同じ
  • なんでも相談室
    • なんでも相談できるらしい
    • 一度行ったが、学生相談室を紹介されただけだった
    • 精神状態の悩みは「学生相談室 or コミュサポ行きましょうね」になるような気がする
  • 工学部学生相談室
    • 学生相談ネットワークの管轄ではなく、工学部が独自に設置している学生相談室
    • カウンセラーじゃなくて事務員的な人が常駐してる?
    • 1回行ったけど正直微妙だった

まとめると、保健センター精神科でおくすりをもらいつつ、学生相談室とコミュサポのどちらか or 両方でカウンセリングを受けるのが良いんじゃないかな。 悩みを言語化したり、自分の状態をより深く理解したりすることはとても大事です。

どうやら自分は研究をしたいわけではないらしい

そんなもろもろの「あがき」や学生相談ネットワークの支援によって現在の私の精神状態はかなり安定しています。 そして今は何をしているかというと、大学院を休学してスタートアップ企業でエンジニアをしています。 今やってることについては他の記事*11で語るとして休学に至った心境についてお話します。

鬱の傾向が晴れて今の自分の状況が冷静に捉えられるようになった私は極めて単純な心理に気付きました。 つまるところ、自分は研究をやりたいわけではなかったのです。 大学に入ったときは講義を受けて色々なことを学びたいと考えていましたし、大学院入試をした動機の中には他の研究者が書いた論文を読むのが好きという要素も多少ありました。 しかし、自分が論文を書いて他の研究者に読ませることには全く興味がないということに気付きました。

本当にやりたいことは研究ではなかった、他にやりたいことがあるはず、仮にやりたいことがまったくなく自分の人生に意味がないとしても研究するよりはマシな時間の潰し方はある。。。 と、あれこれ考えるうちに、嫌々ながら研究を続けるより休学してエンジニアとして働いたほうが良いのではないかという結論に至りました。

  • 自分はお金とスキルを得られてうれしい
  • 先生は私の指導に当てていた時間を自由に使えてうれしい
  • 企業は私という労働力を得られてうれしい

などなど、私が休学して働くことであらゆるリソースが正しく使われるようになるわけです。*12 もちろん修士卒の肩書が遠のくというデメリットもありますが、研究に対するモチベーションが低い自分が、なんちゃってMaster of Engineeringで不当に高く評価されるのは、今後の人生において良い結果を産むはずがありません。

そんなことを考えた私は、次の日には休学届を完成させ、次の日には現在のインターン先に面接に行き(あっさり採用してくれた弊社に感謝)、あっという間にエンジニアに転生していました。 こうして研究する気がない学生が一人消え、人手不足のIT業界に働き手が一人増え、世界は少し良くなったとさ、めでたし。めでたし。

*1:研究室見学では論文執筆環境はあんまり語られることがないが、それなりに重要な要素だと思う

*2:今年秋入学で3人入った。彼らが秋入学したのと入れ違いで休学したので、どんな人なのかあまり知らない

*3:教授や院生が研究テーマを考え下級生にやらせる現象

*4:学内の病院的な施設

*5:精神科医による診察結果であるが、診断書が発行されるレベルの本格的な診断ではないため「傾向」というファジーな表現がされている

*6:この2冊は鬱の状態で読むには内容的にも物理的にもやや重いのでそれっぽい入門書を図書館で借りたほうがいいかも

*7:この本は私が発注した(ドヤ顔)ので工学部2号館図書館に蔵書されている

*8:瞑想をして精霊を呼ぶ的なやつ

*9:レンタカーで四万温泉にドライブして事故る(駐車場で隣の車を擦った)。レンタカー屋の保険2100円で全部解決できた上に警察に通報して事故の後処理をするという貴重な体験ができたのは良い買い物だった

*10:バラマキではなく医学研究の一貫である。念の為

*11:退学アドベントカレンダー13日のエントリ

*12:このあたりやたら大げさに書いているが、休学決めた日は鬱の傾向が裏返って軽い躁状態になっていたのか「俺は休学して世界を正しくするぞおおおおおぉ」と本気で考えていた

クックパッドインターンでちょっぴり成長した

10 day 技術インターンシップに参加してちょっぴり*1成長してきました。かなり楽しかったので体験記を書きます。

internship.cookpad.com

前半5日間

前半の5日は講義形式でwebアプリケーションの開発について学びました。

初日はオフィス見学と環境構築で、2~5日目で以下のようなことを学びました。

  • 事業開発
  • サーバサイド(Ruby on Rails)
  • フロントエンド(React Native、Typescript)
  • クラウドインフラ(AWS、Docker 内製デプロイツールの itamaehako のハンズオン*2 )

個人的にはクラウドインフラの回が一番楽しかったです。 EC2やRDSのインスタンスを立てたり、ロードバランサを置いて負荷分散をしたりといったことを体験しました。 これらは学生が個人でやるには限界があり、ここで体験できたのはとても良かったと思います。 サーバの設定内容をコードの形式で保存しChefツールで環境構築を自動化することで、サーバ環境構築の冪等性を確保するという概念を知ることもできました。

また、実習として、ISUCON形式でアプリケーションの処理速度をインターン参加者同士で競うということも行われました。 私は2位に入賞することができ、賞品としてレシピ本をもらいました🥈

後半5日間

後半はOJT*3コースとPBL*4コースに分かれて参加者ごとに異なる活動をしました。 OJTは実際にクックパッドの開発現場に参加するコースで、PBLは与えられた課題を解決するようなアプリケーションをチームで開発するコースです(やや期間が長いハッカソンのようなもの)。 私はPBLコースに配属され、「食材の購入を支援するアプリを作る」という課題に3人のグループで挑戦しました。

我々のチームは「休日に1週間分の食材を買い込んで、平日はそれらを使って料理する」というシナリオを想定し、1週間分の献立を決めると必要な食材の総量を計算してくれるアプリを作成しました。 自分は主にクライアントサイドの開発担当としてひたすらReact Nativeを書きました。 React Navigationで画面遷移を実装したり、Native BaseでUIを設計したり、コンポーネントはできる限りSFC*5になるように書いて使いまわしやすくしたり実践的なReact Nativeの開発ができました。

ただ、ライブラリの選定や設計方針を予め相談した方が良かったのかもしれません。今回のアプリではヘッダ部分をReact Navigationが生成するものを使いましたが、個人的には使いづらいように感じる部分もありました。 React Native Navigation等の他のライブラリを使用する、ヘッダ部分もNative Baseで実装するという選択肢をあまり考慮せず、なんとなくで決めてしまったのは良くなかったと思います。

仮に今回作ったものを実際に事業として動かすのであれば、デザイン面*6と技術面*7での課題を解決するためにユーザインタビューや機械学習モデルの構築などが必要で、事業開発のほんの一部分しか体験できていないようにも思いました(インターンの期間中にユーザインタビューまでやるのは無理そうだけど)。

参加前はOJTコースで現場の業務環境を体験したいと考えており、PBLコースに配属されたのを残念に思っていました*8。 しかし、アプリの設計から実装まで技術レベルが同じぐらいのメンバーでグループ開発するという体験はそんなにできることではないため、それはそれで良い体験になったのではと思います。

感想

いろいろなことが体験できた10日間でした。 Ruby on RailsやReact Nativeといったフレームワークを初体験したり、インフラエンジニアリングという分野のことを知ったりすることができました。

正直、今回のインターンそれ自身による成長はそこまで大きくは無いと思います。本当に成長するのは今回触れた技術をアルバイトなどで活用したときになるのではと思います。 しかしながら、今回のインターンを通じてweb系エンジニアの方々の働き方を垣間見たり、同期の学生達が様々なことに挑戦していることを知ったりして、勉強していくモチベーションが高まりました。

このインターンで成長したというよりは、今後成長する足がかりができた感じがしました。(植物に例えるなら、幹が伸びるというよりは新しい枝が生えたのかな)

学んだこと

ほぼ初見でしたが、2週間で多少使えるレベルにはなったかなと思います。 特にReact NativeはPBLコースの5日で沢山書いたので、その気になれば自力でアプリひとつ作れる程度にはなったかな? Typescriptやweb版のReact.jsの理解も深まりました。

  • 新規事業開発

企業がどのようにして新規事業を立案したり、ユーザのフィードバックを得たりしているのかを体験できました。大学では得られないものだったと思います。 研究のテーマ探しやユーザスタディなどでも役に立ちそうだと思いました。

新しく興味を持ったこと

  • インフラの最適化 ISUCON

AWSへのデプロイや、クラウドインフラの最適化は独学では体験しにくい分野なので体験できてよかったと思います。 インフラエンジニアという職種を知ることもできました。 また、社内ISUCONを体験して本家ISUCONにも興味が湧きました。

  • Rust

インターンの内容とは無関係ですが、参加者にRustaceanがいて布教されました。 Web Assemblyと組み合わせて使ってみたいです。

その他

クックパッドはオフィスにキッチンがある変わった会社です。 そのキッチンを使って、インターン参加者や社員の皆さんとごはんを作りました。 大勢で料理をしてみんなで食べるというのはとても楽しかったです。それに料理も美味しかったです。餃子とか唐揚げとか中華スープとか🥟 f:id:pf_siedler:20180913154158j:plain 3日目の餃子パーティー f:id:pf_siedler:20180913154204j:plain 最終日の懇親会

*1:圧倒的成長というが界隈で使われていますが、「圧倒的」という部分に嘘っぽさとブラック労働的な苦労自慢のニュアンスが感じられて好きではないので「ちょっぴり成長」したことにします

*2:どちらもオープンソースとして公開されている

*3:On-the-Job Training

*4:Problem Based Learning

*5:Stateless Functional Component

*6:献立を立てる→必要な食材を計算というコンセプトなので、献立を立てるという体験を楽しくさせるデザイン上の工夫が必要だと考えられる。また、世間一般の消費者は休日にまとめ買いや献立を予め決めるといった行動をどの程度行っているのかを分析し、このアプリが本当に必要とされているか見極めなければならない

*7:クックパッドのレシピに記載されている材料欄をもとに買うべき食料を算出しているため、食材を1人分に正規化したり、「適量」とか「お好み」とかいった表現をいい感じの数量に変換したり等で、提示する食材量の妥当性を確保する必要がある

*8:OJTコースのみ時給が支払われるという待遇の違いもあったが、その点について不満はなかった