ソフトウェアエンジニア採用におけるコーディングテストのススメ

先日、某VC投資先の方々に対して、「ソフトウェアエンジニアの採用時にコーディングテストをやりたいがどうしたら良いか?」ということについて語ってきたので、こちらにもエッセンスをまとめたいと思います。

コーディングテストの目的

なぜ我々はコーディングテストをやるのでしょうか?

もちろん、第一目的はソフトウェアエンジニアの採用候補者のスキルを見極めるためです。

過去に、経歴も良さそう、技術的な議論もスムーズにできる、なのにコードが書けない候補者に、私は何度か出会っています。「コードが書けない」のレベルは、(ある程度易しい)論理をプログラムに翻訳できず、まともな if 文が書けないというレベルを言っています。熟練者でもド・モルガンの法則をうっかり間違えるぐらいはあると思いますが、そういう話ではありません。コードが書けない候補者は、そもそも条件が書き下せません。このような候補者を雇ってはいけません。でも、経歴が良く、議論もスムーズなので、第一印象はとても良いです。しかし、コードは書けない。コーディングテストを実施することが、このような候補者を見分ける簡単な方法です。

ところで、コーディングテストの副作用として、自社の技術力を候補者に一定示すことになります。このコーディングテストに通った人しか採用されてないなら、自社メンバーに一定の技術力が見込めると採用候補者に思ってもらうことができます。また、コーディング面接の形式でコーディングテストを行う場合、技術議論によって、こちらのレベル(高いも低いも)を示すことになります。良い候補者にとっては、面接自体が魅力づけになり得ます。コーディングテストをやっていないという理由で選考を辞退する候補者の話もしばしば耳にします。

自社で見極めたいスキルを洗い出しましょう

コーディングテストで見極められるスキルには、例えば以下のようなものがあります。何を重視するかを自社で考えてください。

  • 知識・経験
    • プログラミング言語・周辺環境
    • フレームワーク
    • アルゴリズムやデータ構造
    • セキュリティ
  • 能力
    • システム設計能力
    • コーディングの速さ
    • コーディングの正確さ
  • チーム開発の態度
    • 適切なコメントを書くか?
    • 変数名・関数名は意味がわかるものをつけているか?
  • 問題に向き合う態度
    • 問題の解を考える前に、問題をきちんと理解することに努めるか? いきあたりばっかりか?
  • 議論に向き合う態度
    • 議論になった時に、正しい着地点を見つけようとするか? 自分の意見を通そうとするか?

コーディングテストは、あくまで「ワークサンプルテスト」であるべきです。すなわち、自社での仕事を擬似的な環境で行ってもらうものです。自社であまり使わないような知識を重視して聞くべきではありません。自社で大事だと考えているスキルを問うようにします。チーム開発が大事だと考えているならばチーム開発の態度を問うべきです。コーディングテストというとアルゴリズムの試験をすれば良いと思ってしまいがちですが、難しいアルゴリズムを理解していることが重要な会社ならそうするべきですし、使わないならばそこまで重視するべきではありません。もちろん、アルゴリズムとデータ構造の最低限の知識・経験は全てのソフトウェアエンジニアがしっておくべき知識ですから、候補者に求めて良いとは思います。

コーディングテストの形式

採用において重視するスキルを洗い出したら、形式を決定します。

コーディングテストは、大きくわけて3つの形式が考えられます。

  • 宿題形式
  • 試験形式
  • 面接形式

自社で、採用候補者の何を測りたいのかを中心に、自社で可能な形式かを合わせて形式を決定します。場合によっては複数の組み合わせ (宿題 → 面接) にします。

宿題形式

宿題形式は、期間を設けて、課題を解いてもらい、提出する形式です。

  • pros
    • 多少難しい問題、込み入った問題が出題できます。
    • コメントの書き方、変数の命名法なども見れます。
    • git の履歴などを提出してもらうこともできます。
  • cons
    • 不正が稀にあります。
    • 実際の回答時間がわかりません。
    • 想定外の回答をされても修正することができません。
    • 技術的な議論はできません。

試験形式

オンラインで問題を解答・提出してもらう形式です。

  • pros
    • 実施が楽で、足切りには最適です。
  • cons
    • 不正が稀にあります。
    • 問題のレベルを動的に変更しづらいです。
    • 技術的な議論はできません。
    • フレームワーク系の知識は聞きづらいです。

面接形式

面接内で問題を解いてもらう形式です。

  • pros
    • 不正しづらいです。
    • レベルを動的に変更できます(ヒントや発展問題)。
    • 技術的な議論が可能です。
    • コーディングの過程を見ることができます。
  • cons
    • 面接官の技術レベルが必要です。
    • 短い時間で聞けることしか聞けません。
    • チーム開発の作法はあまり聞けません。

評価ポイントを決めましょう

面接での評価ポイントは予め決めておきましょう。問題が解けたら通す、というのは足切り以外ではあまり良いやり方とは言えません。

大きくわけて、評価には次の2つの考え方があると思います。

  • 育成コストを払えないので即戦力が欲しい
  • 即戦力でなくても良いが、育成可能な候補者が欲しい

即戦力が欲しい場合は、言語知識、フレームワーク知識などの、すぐに使える能力を持っているかどうかを中心に見るべきです。

一方、育成して戦力になれば良いという立場であれば、言語知識やフレームワーク知識を問うより、最低限の知識と、良いお作法でものを考えてきたか? のようなものを中心に見るべきです。

私は後者の立場で見ている場合が多いです。個別の言語知識やフレームワークの知識は、良いお作法でものを考えてきたならば業務内で身につけられると考えており、不足をそれほど問題にしていません。一方、考え方は積み重ねが必要で、簡単には身につかないと考えているため、こちらを重視して評価しています。この考え方は、育成コストはある程度払うので、レベルがあがりやすい特性を持つ候補者を選別している方法だと言えます。また、良い考え方をしていないと思った場合、他社でテックリードをずっとはってきたような方でもお断りしています。

コーディングテスト(面接形式)の実施方法

面接形式の実施方法はさまざまです。google docs のようなものに書いてもらっても良いですが、IDE を使いたい候補者もいるでしょう。こちらは、柔軟に対処できるのが一番良いと思っています。

meety たてときました

面接の悩みを共有できるかなと思ってカジュアル面談をたてておきましたのでお気軽にどうぞ。

meety

(どうでもいいけど hugo で OGP 画像を貼る簡単な方法がわかりませんでした) 

2021-10-28

起業して1年半が経ちました

2020年2月にほぼ個人会社である lambdabox Inc を立ち上げ、2020年4月に @asanokoji らと株式会社ナレッジワークという BtoB SaaS の会社を立ち上げました。

lambdabox Inc の方は今特に述べることがないので(お仕事は受けております)、株式会社ナレッジワークの現在について少々述べようと思います。

株式会社ナレッジワークは当初7人で創業しましたが、今は14人まで拡大しています。プロダクトを作っている人間 (プロダクトマネージャー、ソフトウェアエンジニア、デザイナー、他) と売っている人間(BizDev、セールス、他) とで 2:1 ぐらいの割合です。他、コーポレート系も少々いますね。人数としては当初から2倍になったので、ちょっとは大きくなったなあという感想です。副業という形でお手伝いいただいている方もいくらかおります。

できる喜びが巡る日々を届ける

まずそもそも何をやっている会社なのかという話ですが、ここまでプロダクトはステルスでやってきており、まだしばらくステルスが続く見込みで、多くを語ることはできません。今語れることは、会社のビジョンとして「できる喜びが巡る日々を届ける」というビジョンを掲げており、事業ドメインを「ワークエクスペリエンス領域のイネーブルメント」、すなわち、普段の仕事がよりできるようになるためのプロダクトを提供すること、と設定していることぐらいです。労働は苦役なりという思想から抜け出し、仕事が少しできるようなる→仕事が少し楽しくなる→楽しいからもっとできるようになる→もっと楽しくなる……というループを回せるようにしたいと思っています。

このあたり、自分の原体験からも来ている部分もあり、昔から(高校生ぐらいから)どうやったら何かが「できる」ようになるかということばっかり考えていました。大体は、適切な道筋が示されていないことと、示されたとしてもそれが正しいのか自信が持てないために前に踏み出しづらいことに起因すると思っています。最初の一歩は常に難しいものです。自分はたまたま適切な道筋を見つけ、その道筋の一歩目も上手くいった結果、ある程度までいろんなことができるようになったと考えています。自分だけがたまたまできたということには全く再現性がないので、多くの人に自分なりの道筋を見つけてもらい、最初の「できる」を獲得し、結果自分の力で前に進んでいけるようになってほしいと考えています。

プロダクトと今の会社の状態

イネーブルメントを構成する要素はいくつかあると考えており、ナレッジワークではそれをプロダクトに落として提供しようとしています。プロダクトでは、コアとなる領域の MVP (Minimal Viable Product) をまず作ってお客様からフィードバックをもらって改善しつつ、そちらの手応えが見えた段階でさらに周辺領域の MVP 開発を複数進めてきました。

コアとなる部分は一定の手応えを得ています。自分は現実主義なので多少割り引いてみていますが、一部の経営者であればこの結果であればアクセルをベタ踏みしたくなってもおかしくないでしょう。

一方で周辺領域は、良さそうな兆しが見えたもの、まだまだ作り込みが必要なもの、何をやったらいいのかわかっていないものなどが混在している状態です。まだまだやるべきことが山積みですが、プロダクトマネージャー、ソフトウェアエンジニア、はては BizDev も足りていないため、ある領域を少し良くしてはフィードバックをお願いし、そのフィードバックを Biz が得て新しい仮説をたてている間に別の領域の開発をしているみたいな状態になっています。もう少し人がいればそれだけ仮説検証が回せるのに……と思っています。

プロダクトのコアに関しては、設計を初期にそこそこちゃんとやったおかげで、どんどん変わる要件に対しても内部のモデルはそんなに破綻せずにここまで来れました。当初からみると機能は非常に増えたのですが、特に破綻なく吸収できています。見通せなかった仕様変更もないわけではないですが、些細な変更ですんでいます。

開発はめちゃくちゃアクティブで、patch (Pull Request) の数がこの1年半で 9000 ぐらいになりました。ソフトウェアエンジニアの人数を期間でならすと大体4〜5人程度でやってきたので、適当にならすと 9000 (PRs) / 18 (months) / 5 (people) = 100 で、一人平均毎月 100 個ぐらいの patch を書いている計算です。営業日が月 20 日あるとすると毎日 5 つぐらいですかね。メンバーは残業も大してしてないので(月10~20時間程度)、結構な生産性が出せていると思っています。そもそもイネーブルメントの会社なので、メンバーをイネーブルメントさせないといけないわけで、そこは今のところはまあまあ上手く行っていると思っています。

メンバーを募集しています

というわけでメンバーを募集しています。作りたいものが山積みで、0→1も1→10もどちらもあるフェーズです(10→100以降はまだないんだけど、これから作りたいと思っています)。

いわゆるスタートアップ企業は玉石混交で、玉なのか石なのかがわからないのがスタートアップ界隈を考えている応募者の悩みの1つだと思います。だから、キャピタルゲインを求める人は当たった時の取り分が大きなシードステージへ行き、ある程度安定を保ちつつ挑戦したい人は、当たりそうな名前が知られてきたミドル〜レイターステージ以降あたりを目指しているのではないかと勝手に思っています。ナレッジワークはシードよりであり、今の段階では当たった時の取り分もまあまあ大きくて、そして結構当たりそうな感じ(自画自賛)もあるので、実は結構お得感のあるフェーズではないかと考えています。実際ストックオプションも全員にかなり厚めに配布しているつもりで、そして、早く参加していただければそれだけ多く出せます(出せる量に上限があり、どうしても頭割りになっちゃうので人数が増えれば増えるだけ少なくはなります)。

ご興味を持たれた方は、リクルートページ からご応募いただくか、もしくは @mayahjp まで DM をください(DM は開放しています)。

他のメンバーのエントリ

補遺

プロダクトの構成についても今後書いていきたいと思います。

2021-09-28

競技プログラミングという言葉の誕生

一定時間内に題意を満たすようなプログラムを、よりたくさん、より高速に書くことを競うプログラミングコンテスト、およびその形式のことはしばしば「競技プログラミング」と呼ばれています。今となっては当たり前となった言葉ですが、私が現役でこのような競技に参加していた頃 (2005年まで) はそのような言葉はなく、単に「プログラミングコンテスト」と呼ばれていました。

さて、競技プログラミングという言葉はいつ生まれたのでしょうか? 一部では “competitive programming” の訳語であると思われていますが、実はそうでもないのです。

自分の体験談を少し振り返ると、私が YUHA で競技プログラミングに関連する同人誌的なものを書き始めた 2008 年夏 (C74) には、「競技プログラミング」という言葉はまだ広くは知られてはおらず、同人誌中でも競技プログラミングという言葉は使われていませんでした。2010 年夏 (C78) に発行した同人誌からタイトルに「競技プログラミング」という言葉が使われ始めています。この頃売り子も自分がやっていましたが、「競技プログラミング」という言葉は全く浸透しておらず、興味を持ってもらった人に「競技プログラミング」とは何かを一から説明するような状況でした。ただ、「競技」という言葉はキャッチーだったようで、2010 年冬 (C79) に発行するものからは「競技」という言葉を前面に出したことを覚えています。それでも浸透度は低く、数年は「競技プログラミング」という言葉の説明が必要でした。2019 年現在「競技プログラミング」という言葉は十分に浸透していると思われ、もうプログラマ向けには説明はほとんど必要がないと思われることを考えると、隔世の感がします。

さて、twitter で [2008 年 12 月 31 までの期間で競技プログラミングという言葉を検索してみる](https://twitter.com/search?q=競技プログラミング until%3A2008-12-31)と、その言葉が使われ始めた萌芽はあるものの、出てくるツイート数も少なく、まだ一部でしか浸透していないことがわかります。Google Trend で検索してみても、2004, 2005年になぜか突発的に「競技プログラミング」という言葉がトレンドに現れてはいるものの、2007年頃から継続的に現れ始めた言葉であることがわかります。

twitter のこれらの発言 1 2 をきっかけに「競技プログラミング」という単語のルーツを追ってみた結果、どうやら「東京大学競技プログラミングクラブ」という東大の勉強会が存在したことがわかりました。そしてそのクラブ名の名付け親である @ymatsux により、彼の思いつきが発端であるということがわかりました。

それであれば、自分は卒業しているため 2007 年にその言葉を知らず、一方後輩からその言葉を知ったおかげで広く知られる前から使い始めたということで、納得ができます。

なお、@nya3jp のツイートによると、「競技プログラミングクラブ」が発足したメーリングリストの一通目は 2007 年 3 月 30 日ということなので、この日が公の「競技プログラミング」発祥の日なのかもしれませんね。これから毎年祝いましょう。(追記: どうやら発足はもう少し前に遡れるようです。)

なお、英語の “competitive programming” ですが、Google scholar によると、2007 年よりも前からなくはなかったようです。この PDF によると、1999 年にはそのような講義 (CS3233 Competitive Programming in July 1999) があったことが示されています。ただ、ここから日本語の「競技プログラミング」が訳されたというよりは、独立に同じ言葉が生まれたと考えるのが適切ではないか、と考えています。


以下は 2019-07-28 にした追記です。

どうも、「競技プログラミング」という単語はもうちょっとだけ日付を遡れるようで。いわゆる仲間内での合宿(という名前の旅行)に「東京大学競技プログラミング同好会」と名前をつけたのが初出のようです。

というわけで、2007年3月21日には今の意味での「競技プログラミング」が始まっていたようです。

2019-07-25

異世界に転生しました

主に友人知人向けへの周知です。2019 年 3 月末で G を退職しています。

何か新しいことをやろうと思って水面下で様々準備していたのですが、事業を 0 から起こすというのは経営のスキルも足らずなかなか難しいなと攻めあぐねていたところ、前前職 (W) の上司の紹介で VISITS technologies の CTO 職をやってほしいという話になり、社長と話してみるとやりたかった事業ドメインに近かったため、引受けてみることにしました (万が一事業に失敗したらどこか拾ってください)。

いわゆる退職エントリはドラフトを長々と書いてみたのですが、書いた結果あんまり公にすることではないなと思ってお蔵入りにしました。積もる話は直接聞いてください。

現在丸の内勤務になったので、主に丸の内近辺の方(わかる範囲だとPFNとかTDとか)、遊んでください。ただ、G の頃より異常に忙しいので(これがスタートアップか)、なかなか時間取れないかもしれません。

こちらからは以上です。

2019-07-24
告知 

パスにおいて /a/b/../c と /a/c は等価か

今、分散コンパイラのキャッシュを作るようなお仕事をやっています。キャッシュを作るときには、キャッシュヒット率を上げるためにファイル名はなるべく正規化して持ちたいと思うことがしばしばあります。

コンパイラですと、-I で指定されたインクルードディレクトリ名とインクルードファイルを join してパスを得ることがあります。インクルードディレクトリやファイルが ../ を含んでいることは多分にあり、もし、../などを解決して絶対パス名にして保持できると、その後の扱いが楽にあります。例えば、パス名として /a/b/../c が与えられたとき、/a/c の形で保持できると少し嬉しいわけです。ファイルにアクセスすると遅いため、できればパス名だけでこのような正規化ができると嬉しいわけです。

さて、このような正規化をしてもいいのでしょうか。ダメだとするとどういう例があるでしょうか。

ダメそうな例として、b がどこかの directory への symlink である場合が考えられます。例えば /s/t への symlink であったとします。/a/b/../c を symlink を辿って解決すると、/s/c になり、/a/c とは違う場所を指しています。

実際、gcc で次のような ファイル構成および内容のとき、

a/b
  s/t への symlink
a/c.h
  #define KOTORI 100
s/c.h
  #define KOTORI 200
s/t
  空ディレクトリ
test.h
  #include <a/b/../c.h>
test.cc
  #include <stdio.h>
  #include <test.h>
  int main() { printf("%d\n", KOTORI); return 0; }

gcc -I. test.c として出来上がった実行ファイルは何を出力するでしょうか。symlink を解決していれば 200 が、解決されてなければ 100 が表示されるはずです。

$ gcc -I. test.c
$ ./a.out
200

実際は 200 となるため、symlink が解決されており、/a/b/../c/a/c は等価ではないという結論になりました。

一見 /a/b/../c/a/c にしたくなるのはわかりますが、これは悪魔の囁きです。

2017-06-05

王様達のヴァイキング×SECCON スペシャルトークセッション 終わりました

王様達のヴァイキングの技術監修役としてSECCON 2016 東京大会でちょっとしゃべってました。

まさか花が飾られているとは……。

普通 IT 系の勉強会だと技術がわかっている人が多いので、その人たち向けの前提知識で話します。今回もそんな感じでいいかなって思っていたのですが、当日どうも女性が結構登録しているらしいと聞いて、これは考えを改めねばと思いました。IT 系の勉強会では残念ながら女性は多くても数人です(女子向けは除く)。ということは、これは技術がわかっている人と、おそらく技術にはそんなに詳しくはないがマンガに詳しい人たちが混ざっている……となるわけです。

両方の人たちを満足させなければならないので、技術的な話とそうでない小ネタ(主に設定)を半々ぐらいのバランスで挟むことにしました。両方の人に満足していただけたのかどうかはわかりませんが、当日最初らへんのしゃべりで反応があった数人にターゲットを絞って、まあ彼・彼女らが満足してればよかろうというスタンスでしゃべっていて、最後までわりと満足そうな顔をしていたので良かったのだと思います。

2017-01-29
告知 

王様達のヴァイキング×SECCON スペシャルトークセッション

王様達のヴァイキングの技術監修役として SECCON 2016 東京大会でちょっとしゃべることになりました。2017年1月28日13:00-14:00に東京電機大学だそうで。

SECCON 2016 決勝大会

セキュリティに関してはどう考えても来場者の方がレベルが高いと思いますが、漫画の屋台骨を支える役にはなんとか立っていると思っています。

2017-01-16
告知 

OpenBLASが速かった

ちょっと行列演算をしたかったので適当にコードを書き散らしていたのですが、速度が必要な演算であるために既成のライブラリを使うこととしました。今回は行列乗算ができれば良いのでBLASを生で使ってもそんなに問題ありません。BLASの中でも高速な実装であるOpenBLASを使い、適当に書いたコードと速度の比較をしてみます。

結論から言うと、OpenBLASの速さを舐めてました。まさか50倍の速度差がでるとは。

比較に用いたマシンは、MacBook Pro 2013 Late 13 inch (2.4 GHz Intel Core i5) です。

インストール

Macの場合はbrewで。LinuxでもOpenBLASのパッケージはたいていのディストリビューションにはあると思います。

$ brew install homebrew/science/openblas

Matrix クラス

1024 × 1024の大きさの実正方行列の掛け算をします。以下N = 1024とします。

const int N = 1024;

適当にMatrixクラスを定義します。精度はそこそこで良いので今回はfloatを使います。

class Matrix {
public:
    Matrix(int row_size, int col_size) :
        row_size_(row_size),
        col_size_(col_size),
        data_(new float[row_size * col_size]) {}

    float& operator()(int row, int col) { return data_[row * col_size_ + col]; }
    const float& operator()(int row, int col) const { return data_[row * col_size_ + col]; }

    float* data() { return data_.get(); }
    const float* data() const { return data_.get(); }

private:
    int row_size_;
    int col_size_;
    unique_ptr<float[]> data_;
};

operator()を定義していて、行列Ai行目j列にはA(i, j)でアクセスできるようにしておきます。

1024 × 1024 の行列 A, B を掛け算し、C = ABを計算するものとします。C++の変数としては小文字a, b, cを用いて次のように表すとします。

Matrix a(N, N);
Matrix b(N, N);
Matrix c(N, N);

ナイーブに掛け算をした場合

ナイーブに実装すると次のようになるでしょう。

for (int i = 0; i < N; ++i) {
    for (int j = 0; j < N; ++j) {
        float s = 0;
        for (int k = 0; k < N; ++k) {
            s += a(i, k) * b(k, j);
        }
        c(i, j) = s;
    }
}

この場合、b(k, j)の部分がメモリを飛び飛びにアクセスすることになるので速度はでません。clang -O3で試すと、おおよそ6〜7秒程度の時間がかかります。

乗算順序を変更

i, j, kの順序でアクセスするのではなく、i, k, jの順でアクセスするとメモリアクセスが飛び飛びにならないのでいくらか速度が稼げます。 ただしc(i, j)は0で初期化しておく必要があります。

for (int i = 0; i < N; ++i) {
    for (int k = 0; k < N; ++k) {
        for (int j = 0; j < N; ++j) {
            c(i, j) += a(i, k) * b(k, j);
        }
    }
}

これでおよそ1.1〜1.2秒ぐらいです。演算回数としては10243なので1G回の演算があるわけですが、2.4GHzのCPUで1.2秒となると、まあちょっと遅いですねという気持ちになります。

OpenBLAS の場合

関数cblas_sgemmを使います。cblas_sgemmは、行列A, B, Cとスカラーα, βαAB + βCを計算します。計算結果はCに格納されます。単に掛け算ABを計算したければ、βを0とすれば良いでしょう。

cblas_sgemmの引数はちょっと見ただけではわかりづらいです。

行列の要素はメモリ上では行ごとに並んでいるので、CblasRowMajorを指定する必要があります。FORTRANなどではメモリ上は列ごとにならべるのが普通ですがC/C++では行ごとにならべるのが普通です。また、cblas_sgemmでは転置してから行列を掛け算することもできます。今回は転置などせずに掛け算するので、CblasNoTransを指定します。

cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
    N, N, N,
    1, a.data(), N,
    b.data(), N,
    0, c.data(), N);

これの計算時間はわずか0.03秒でした。

ちなみにEigenでも同じような計算をしてみたのですが、こちらは0.11秒程度でした。それでも速いですね。

2016-01-17