freee株式会社を退職しました
この記事は退職者アドベントカレンダー2017(その2)の15日目です。
今年の10月いっぱいでfreee株式会社を退職しました。 どこか時間がある時に退職エントリー書くかーと思ってたのですが、そうこうしているうちにアドベントカレンダーの時期になってしまったので、この機会に書くことにします。
freee株式会社でやっていたこと
Webアプリケーションエンジニアとして、サーバサイド、クライアントサイドを問わずアプリケーションの開発業務を中心に従事していました。ある機能の開発サイドのオーナーみたいなことをやっていたこともありました。これは自分の中でも特に良い経験でした。
言語的にはRubyとJavaScriptを書いている時間が長かったです。あと社内にボドゲが大量にあったので、暇な時にみんなで集まってやっていました。
振り返る
僕が入社したのは2015年の4月だったので、ちょうど2年半ほど在籍していたことになります。
入社当時の社員数は80名弱でしたが、退職時点ではその4倍を超える社員数になっていました。 すごい速度で会社として成長してきたんだなーと改めて思います。 2、3日おきに10人ぐらい入ってきていて、もう顔がわからん..みたいなことになる時期もありました。
この成長期を実際に中の人として経験できたのは、自分にとって本当に良かったと思います。会社が成長していく中でのなんとも言えない熱狂と、過程で生じる摩擦も含めて、なかなか得ようと思っても得られない経験だったと思います。
freeeは率直に言って働きやすい会社だったと思います。というと退職エントリーあるあるな前職を持ち上げるあれっぽいですが、まぁ率直な感想です。 出社退社時間や働く場所(リモート)の調整は自分の裁量で決めることができました。僕にはコードに熱中し始めるとある程度まで書き切らないと止まれなくなる悪癖があるので、すごくありがたかったですね。朝会あんまり出席してなくてすみませんでした。
なぜ退職したか
freeeの仕事に特に不満はありませんでしたが、どんどん拡大していく開発組織に戸惑いはありました。 ジェフ・ベゾフのTwo-Pizza Team Ruleの考え方が結構好きなのですが、開発組織の人数を増やす前に生産性をあげる手段は別にあるんじゃないかな、と思うことが増えて、わずかですが外を意識し始めました。
単純に2年半もいると中で使っている技術については熟知してしまって、刺激不足だったというのもあります。
とは言えfreeeのプロダクトの目指しているビジョンと、難しい領域へのチャレンジには共感していたので、それらがあってもまだやめる段階ではないと思っていました。むしろ自分でやっていきを発揮して何とかしてみようと思ってました。何とかできたかはわかりませんが。
自分の感覚的に、あと2年ぐらいでこの界隈の勢力争いに一定の区切りがつきそうだなと思っていたので、少なくともそれまでは何もなければ在籍しているつもりでした。
しかしそんな折に、知人から教育系のWebサービスのベンチャーの誘いを受けて、迷いましたが、彼の熱意とプロダクト・働き方の両面での面白さに惹かれて2人目の社員としてジョインすることにしました。
彼はプロダクトをすでに持っていましたが、プロダクト、今後の展開の両面でのフルリニューアルをしている真っ最中で、そのタイミングでのお誘いでした。 プロダクトについては、特にフロントについては全面書き換えということで、全てにおいて自分の裁量で決定できることに魅力を感じました。 Websocketを用いたリアルタイムなインタラクションやPWA対応が必要な要件で、特に興味がある領域だったことも大きいです。
展開については、海外に会社をもち、そこを拠点として展開させていきたいという話を受けました。現在事務処理を大急ぎで頑張っている(僕は何もやってないですが)ところなので詳細は避けておきますが、うまく行けば来年には海外に会社が建つことになります。僕も来年少なくとも半年以上はそこで過ごすことになると思います。以前より英語圏で仕事がしてみたいと思っていたので、これは非常に魅力でした。
あとは結構な額のストックオプションももらったので、当たればでかいというのも当然あります。一発あてたい。
僕も今年で30だしこの辺りで挑戦しておくか、という気持ちになったというのもあります。
実は10月に結婚もしているんですが、妻はあっさりokしてくれました。
現職については、また落ち着いたら色々書きたいです。技術的にも面白いことができているので、どこかの機会に発表もしたい。
というわけで
これからもよろしくお願いします!
Vue/Vuexに静的に型を付ける
最近Vue/Vuexを触っている。
前々から欲しいと思っていたのもあって、習作としてelectronでYouTubeのデスクトップクライアントを作った。
僕は仕事中はだいたいYouTubeを再生している。映像を見ながらコードを書きたい欲求があった。とはいえ、そのために作業領域を侵食されるのはつらい。 ということで前面に固定する機能と透過率を設定できる機能をつけた。 こんな感じになって便利。
Vueにおいて、TypeScriptを選ぶかFlowを選ぶか
Vueにおいて、楽をしたいならTypeScriptを選ぶ方が良い。Flowに比べて、公式のサポートが断然に厚い。 公式ページにもサポートについて1セクション割かれている。
また、TypeScriptチームがVueとTypeScriptのStarter Kitを公開している。
最初のセットアップはこれを参考にすると捗る。
注意点として、このPRのpackage.jsonのvueの依存は以下のPR内のコミットに向いている。
現在最新であるv2.4.2においてはまだマージされていないが、これが入ると、VueのComponentのオプション内でthisが推論できるようになる。
詳しくは@ktsnさんの以下の発表資料を見るとわかりやすい。
TypeScriptにおいてはfunctionのthisをパラメータとして受け取れるゆえこれができるんだけど、さらにFlowサポートは困難になりそうな気配を感じる。
https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters
これがマージされていない現在においては、vue-class-componentを使って、Componentをクラスで定義するのが現実解だろう。
余談だが、vue-class-componentではPropとWatchはアノテーションでは表現できない。
これはBabelとTypeScriptのトランスパイルの挙動の違いによるようだ。
TypeScriptを使うなら、vue-property-decoratorがこれをサポートしている。
Vuexの提供する単一方向フローへの型付け
VuexはVueにFluxの単一方向フローによるデータ更新を導入するものだ。
Vuex自体はすごく使いやすく、Fluxに触れたことがあればわかりやすいんだけど、提供しているAPIは各レイヤーを跨ぐ時に型安全性を維持するのが難しい。(理由などはあとで細かく各レイヤーの型付けを見る時に説明します。)
既存のVuexの実装に無理やり型付けするのはかなり厳しかったので、Vuexの薄いラッパーを作った。
Actionの定義
Vuexの提供するactionの定義は以下のようになる。
actions: { hoge (context, payload) { context.commit('hoge', payload) } }
第1引数にactionのcontext、第2引数viewから渡されたpayloadを受け取る。 actionのcontextは、ここからmutationへ渡すための関数であるcommitやstateやgetterなどのプロパティを含む。 (型定義を直接見た方が何が使えるか把握しやすい。)
ReduxにおけるActionCreatorは、以下のようにviewから直接呼べる形で定義するのでViewから呼び出す場面で型付けがしやすい。
function addTodo(text) { return { type: ADD_TODO, text } }
Vuexの提供している形式では、ViewにはもちろんActionのcontextなどはないので、定義をそのまま使って型安全を保証するのは困難だ。
そこでactionの定義は改変することにした。 具体的には以下のようにした。
import { ActionCreatorHelper } from 'battle-ax'; import { Product } from '../types'; import { State } from './index'; import shop from '../api/shop'; export type ActionTypes = { ADD_TO_CART: { id: number }, CHECKOUT_REQUEST: null, CHECKOUT_SUCCESS: null, CHECKOUT_FAILURE: { savedCartItems: Product[] }, RECEIVE_PRODUCTS: { products: Product[] } }; export const actions = ActionCreatorHelper<State, State, ActionTypes>()({ addToCart (payload: { id: number }) { return ({ commit }) => { commit({ type: 'ADD_TO_CART', payload: { id: payload.id } }); } }, //...省略 }
Actionはpayloadを第1引数として受け取り、ActionContextは返り値の関数定義にinjectされるようにしている。 こうすることで、Viewから直接呼び出せる形のAPIになったので、Viewの呼び出し時に型付けすることができる。
BattleAXの提供するActionCreatorHelperは、<State, RootState, ActionTypes>をそれぞれgenericsとして受け取る。 ActionTypesはkeyがmutationのkey(fluxにおけるtype)、valueがpayloadになる。 こういう定義にしているのは、mutationで受け取る時にkeyの一致判定でpayloadの推論ができるようにしているためだ。(詳しくはmutationの説明で。)
BattleAXのAction関連の定義は以下のようになっている。
export interface ActionContext<S, R, A> { dispatch: Dispatch; commit: <K extends keyof A>(params: { type: K, payload?: A[K] }) => void, state: S; getters: any; rootState: R; rootGetters: any; } type Action<S, R, A, P> = (payload: P) => (injectee: ActionContext<S, R, A>) => any; export type ActionTree<S, R, A> = { [key: string]: (payload: any) => (ctx: ActionContext<S, R, A>) => any } export function ActionCreatorHelper<S, R, A>() { return <T extends ActionTree<S, R, A>>(ac: T): T => ac }
TypeScriptのMapped Typesを使って、commitの型安全を保証している。
commitの引数が定義を満たさなければ以下のようにちゃんとエラーになる。
ActionCreatorHelperは見ればわかる通り、何もせずに型付けしてそのまま引数を返しているだけ。 ActionCreatorHelperは自身が関数で、payloadを受け取る関数を返すという若干奇妙な感じになっている。
本当は ActionCreatorHelper<S, R, A, AC>
(ACは引数で受け取るActionCreatorの関数群)という感じにしたかったんだけど、このACだけは推論を効かせたかった。
TypeSctiptのgenericsは、1部のみに推論を効かせることはできない。ヒントを与えるなら全てにヒントを与えなければいけない。 そこで事前に定義をしなければいけない要素を増やすのは冗長すぎるので、関数として分けることで対応した。 他に良い方法があれば知りたい。
Mutationの定義
Vuexの提供するMutationの定義は以下のようになる。
mutations: { ['hoge'] (state, payload) { state.hoge = payload.hoge; } }
ActionCreatorでcommitされたkeyで定義した関数が実行される。この関数は、第1引数にstate、第2引数にcommitされたpayloadを受け取る。
Reduxでは、dispatchされたActionはreducerでそのまま受け取り、Actionのtype propertyで処理を分岐する。
function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } }
静的型付けの観点では、ActionTypeをunion typeとして定義しておき、type propertyの動的チェックにより絞り込みを効かせることで型安全を保証するのが一般的だ。
BattleAXでは、先ほど定義しActionTypesの定義(keyがmutationのkey、valueがpayload)をgenericsとして受け取る。
import { MutationTree } from 'battle-ax'; import { ActionTypes } from './actions'; import { State } from '../store'; export const mutations: MutationTree<State, ActionTypes> = { ['ADD_TO_CART'] (state, payload) { state.lastCheckout = null; const record = state.added.find(p => p.id === payload.id); if (!record) { state.added.push({ id: payload.id, quantity: 1 }); } else { if (record.quantity) record.quantity++; } const target = state.all.find(p => p.id === payload.id); if (target && target.inventory) target.inventory--; }, // ...省略 }
ここはVuexのAPIを変更せずに型づけすることができた。 BattleAXでのMutationの型定義は以下のようになっている。
export type Mutation<S, P> = (state: S, payload: P) => any; export type MutationTree<S, A> = { [P in keyof A]?: Mutation<S, A[P]>; };
ここでもMapped Typesを使っている。 受け取ったActionTypesのkeyとmutationのkeyが一致していること、payloadはActionのvalueになること、という定義にすることで型安全性を保証している。 定義を満たしていなければ、以下のようにちゃんとエラーになる。
Getterの定義
Vuexの提供するGetterの定義は以下のようになる。
getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } }
stateを第1引数で受け取り、そこから値を変換している。第2引数で他のgetterも受け取り、使用することもできる。
doneTodosCount: (state, getters) => { return getters.doneTodos.length }
ここのAPIは変えずに型付けすることができた。
import { State } from './index'; import { FullItem } from '../types/Item'; import { GetterTree } from 'battle-ax'; export type Getters = { nextVideo: FullItem | null; nextVideoId: string | null; relatedVideos: FullItem[] }; export const getters: GetterTree<State, State, Getters> = { nextVideo: (state, getters) => { const next = state.relatedVideos.filter((v) => !state.playedVedeoIds.includes(v.id.videoId)); if (next.length === 0) { return null; } return next[0]; }, nextVideoId: (state, getters) => { return getters.nextVideo && getters.nextVideo.id.videoId; }, relatedVideos: (state, getters) => { return state.relatedVideos.filter(v => v !== getters.nextVideo); } }
BattleAXの提供するGetterTreeを利用するためには、genericsとしてGetterの定義を渡す必要がある。 Gettersはkeyがgetterのkey、valueが計算結果になるオブジェクトとして定義する。 BattleAXの型定義は以下のようになっている。
export type GetterResult = { [key: string]: any } export type Getter<S, R, G, V> = (state: S, getters: G, rootState: R, rootGetters: any) => V; export type GetterTree<S, R, G extends GetterResult> = { [P in keyof G]: Getter<S, R, G, G[P]>; }
genericsとして受け取ったGettersは、ここでもMappedTypeを使い、他のレイヤーから呼ばれた時も型を維持できるようにmappingしている。 このGettersはなんとか推論で定義せずに済むかも?と思ったけど、ts力が足りなくてそこまでには至れなかった。
Storeの作成
BattleAXの提供するcreateStoreを使い、storeを作成する。
import { mutations } from './mutations' import { actions } from './actions'; import { FullItem } from '../types/Item'; import getters from './getters'; import { createStore } from 'battle-ax'; export type State = { items: FullItem[], relatedVideos: FullItem[], playedVedeoIds: string[], miniPlayerMode: boolean, transparentRate: number, loading: boolean } const state: State = { items: [], relatedVideos: [], playedVedeoIds: [], miniPlayerMode: false, transparentRate: 0, loading: false } const store = createStore({ strict: process.env.NODE_ENV !== 'production', state, actions, mutations, getters, }); export default store;
Storeをラップすることで、先ほど改変したActionCreatorやMutationの帳尻を合わせてVuexに渡している。 ここでは特に特筆することはないので、気になる方は実装を見てください。
View -> Action
BattleAXの提供するstoreは型付けされているので、View側でimportした時に型安全性を保証できる。 以下のようにactionsの補完も効く。
このままimportしたstoreを使うこともできるが、それではComponentがstoreに依存してしまう。 そうすると、テストをする時に非常に厄介になる。
Vuexでは、rootにstoreをinjectすることでComponentの全てからstoreを呼び出すことができるようになる。
しかし、これでは全てのcomponentからstoreの値をどこでも呼べるようになってしまう。 PresentationalComponentとContainerComponentを分離する上で、これは都合が悪いと僕は思う。
性善説で言えば、再利用性を高めたいComponentではstoreの値を直接呼ばないようにすることで問題は起きないだろうが、人は善だけでは構成されていない。
BattleAXでは、injectという関数を用意した。これは第1引数にstore、第2引数にvue componentを受け取る関数で、これを通した関数はaction, getter, stateがinjectされる。
以下のように使う。
import Vue from 'vue'; import VueRouter from 'vue-router'; import Root from './containers/RootContainer.vue'; import Home from './containers/HomeContainer.vue'; import PlayVideo from './containers/PlayVideoContainer.vue'; import miniPlayer from './containers/miniPlayerContainer.vue'; import store from './store/index'; import { inject } from 'battle-ax' Vue.use(VueRouter); const routes = [ { path: '/', component: inject(Root, store), children: [ { path: '/mini-player/:id?', name: 'miniPlayer', component: inject(miniPlayer, store) }, { path: '/', name: 'home', component: inject(Home, store) }, { path: '/:id', name: 'player', component: inject(PlayVideo, store) }, ] }, ]; export default new VueRouter({ routes });
実装は以下の通り。
export function inject<S, G, A, AC extends ActionTree<S, S, A>>(container: any, store: BAStore<S, G, A, AC>) { const name = container.options.name || 'unknown-container'; return { name: `injected-${name}`, components: { [name]: container }, data: () => ({ actions: store.actions, state: store.state, getters: store.getters }), template: ` <${name} :actions="actions" :state="state" :getters="getters" /> ` } }
HOCパターンを使って、actions、state、gettersをpropsとして注入しているだけ。VueのHOCの実装パターンがよくわからなかったんだけど、これで良かったんだろうか。 これを使うとPropsのバケツリレーからは逃れられないが、お行儀はよくなる。そこはトレードオフだ。
ちなみにViewのtemplateを使うと、その中でアクセスされる変数が本当にあるかどうかのチェックは現状ではできない。 このチェックができないとリファクタの段階においてかなり厳しいので、基本的にtemplateで使う値をstoreから取りたい時には、component内で全てcomputed propertyとして定義することにした。
以下のような感じ。
<script lang="ts"> import Player from './Player.vue'; import { State } from '../store'; import VideoList from './VideoList.vue'; import Thumbnail from './Thumbnail.vue'; import { actions } from '../store/actions'; import { Getters } from '../store/getters'; import { Component, Prop, Vue } from 'vue-property-decorator' @Component({ components: { Player, VideoList, Thumbnail }, }) export default class PlayVideoPage extends Vue { @Prop({ type: Object, required: true}) actions: typeof actions; @Prop({ type: Object, required: true }) state: State; @Prop({ type: String, required: true }) id: string; @Prop({ type: Object, required: true }) getters: Getters; get videoId() { return this.id; } get nextVideo() { return this.getters.nextVideo; } get nextVideoId() { return this.getters.nextVideoId; } get relatedVideos() { return this.getters.relatedVideos; } get miniPlayerMode() { return this.state.miniPlayerMode; } } </script>
めっちゃ冗長だけど、これで変更には強くなる。これもトレードオフだ。 ただここは、技術的にはtemplate内でのthisのアクセスをチェックすればいいだけだと思うので、そこまで苦労せずに事前チェックできそうな気がする。時間を見つけてトライしたい。
まとめ
こんな感じでVue/Vuexを型付けすることができた。 これでだいたい思う通りになったんだけど、SFCを使った時のtemplate内でのcomponentの呼び出し時のpropsの静的チェックができないのが一歩Reactに劣る。
ただこれもktsnさんが手をつけているので、そのうちできるようになるかもしれない。(期待!) github.com
Vueは書いていてすごく楽しかったし、使いやすいなーと思った。
ReactNativeのAsyncStorageをNodeのREPLから操作する
背景
ReactNativeにはAsyncStorageというkey-valueストレージシステムがある。 valueにはstringしか入れられない本当に簡素なものだけど、JavaScriptのプレーンなオブジェクトはJSONにシリアライズ可能であるので、さほど困らない。
クライアントサイドで永続化したい情報はAsyncStorageに突っ込んでおく。アプリケーションを作っている過程において、値を少しづつ変更しながら開発を進めたい場面が結構あった。
ところでRailsにはrails consoleというものがあり、railsアプリケーション内のinitialize処理を通した後のREPLを立ち上げることができる。このREPLでは、railsアプリ内のクラスはすでにロード済みであるため、その全てを呼び出すことができる。その中でデータベースアクセスを担うクラス群も呼び出せるため、rails consoleから自由にデータの参照や改変を行える。
普段Railsで開発していることもあり、そんな感じにNodeのREPLからReactNativeのAsyncStorageにアクセスしたいな〜、と思ってそんなツールを作った。
動作
このアプリケーションは、コメントの投稿と同時にAsyncStorageと同期し、コメントリストを永続化している。 REPLからAsyncStorageにコメントを追加し、その後アプリケーションをリロードした時に、コメントが追加されていることを確認している。
仕組み
NodeとReactNativeとの通信はwebsocketを利用している。(ReactNativeは標準でwebsocketをサポートしています。)
REPL起動時にchild.spawnで子プロセスとしてwebsocket serverを立ち上げて、ReactNativeアプリケーションからのレスポンスを受け取る。 REPLのプロセスとwebsocket serverはプロセス間通信を行う。 通信のイメージは以下
データフローの最後の部分で、websocket serverが結果をファイルに書き込み、それをREPLのプロセスで監視しているところがある。なんで??感がすごい。
今回の構成ではWebSocketを挟んでいたり、そもそもAsyncAtorageはその名の通り非同期なAPIを提供していたりするので、REPLから発行したコマンドがAsyncStorageのAPIを叩いて結果が返るまでは、どう頑張っても非同期処理になる。これを無理やり同期処理にするために、ここでファイルを見ている。
REPLで操作しているのにPromiseが返ってくるのは使いづらい。しかし、Node.jsの世界では、プロセスを止めて処理の結果を待つことが基本的にはできない。(bindingを書けばその限りではないけど。) top levelでawaitする方法があればどうにかなりそうだけど、今のJavaScriptの世界ではそれは許されていない。
Top-level `await` is a footgun · GitHub
というわけで以下のようになった。
execFileで子プロセスで出力処理をさせて、親プロセスではsleepのbindingライブラリで処理を停止(物理)して、出力結果を監視することで無理やり同期的にする荒技に辿り着いた
— じょう (@joe_re) 2017年5月6日
出力結果の監視 is ファイルが書き込まれたかどうか。node-sleepでプロセスを停止させつつ、一定周期でファイル出力がないか監視している。internalなネットワークの通信なので、大抵はほとんど待たずに結果を受け取れるはず。ReactNative側とwebsocketの接続ができていなかったりした場合には、一定時間後にタイムアウトになる。 無理やり感が漂っているので、もっといい方法をご存知の方はぜひ教えてください。
結構便利です
結構便利に使えている。こいつにstorageの状態のスナップショットの取得/復元をする機能とかあってもいいかなー。 興味のある方はぜひ使ってみてください。