$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

どのようにエンジニアの目標設定を行うか

以前 ゴールを決め目標を決める・解決案ではなく質問する - コーチングの学習で学んだこと - $shibayu36->blog; で、「ゴールを決め、現在位置とのギャップを考え、目標を決める」と良いということをまとめた。イメージとしては以下の図の通り。

f:id:shiba_yu36:20161023134206j:plain:h250


しかし、前回の記事だと具体的にどのようにエンジニアの目標設定を行うかイメージが湧かない。そこで、もう少し具体的に最近どのようにやっていたかを書いてみたいと思う。


僕がメンティーと目標設定を行うときは、以下のフローを辿っている。

  • なんでも良いのでゴールのイメージを明確にする
  • 現在の自分とゴールのイメージのギャップを考える
  • ギャップを埋める目標を考え、アクションを定める

ちなみに今回は、チームの成果達成のために個人の目標を決めるのではなく、エンジニアのスキル向上の目標を立てるという前提で書いていく。

なんでも良いのでゴールのイメージを明確にする

まずはいろんな質問や会話をしながら、メンティーが今後どうなっていきたいかなどのゴールイメージを明確にする。


この時に僕はよく以下のような質問をしている。

  • 2~3年後にどういうエンジニアになりたいか(なりたい姿を決める)
  • 1年後にどういう仕事をしていたいか(やりたいことを決める)
  • 最近自分が課題に感じていることあるか(課題を明確をする)
  • 自分がこの半年でどういうことやりそうか やる予定のことを明確にする

もし長い目線で考えることが得意そうな人なら、「2~3年後にどういうエンジニアになりたいか」のような質問をし、なりたい姿を明確にしていく。エンジニア像はどういう決め方でも良く、例えばビジネスも分かる・チームを率いることができる・専門スキルが突出しているなどの能力ベースで話しても良いし、社内のこの人・社外のこの人みたいな誰になりたいかベースで話しても良い。

なりたいエンジニア像がはっきりしない場合、次は「1年後にどういう仕事をしていたいか」という質問をし、やりたいことを明確にしていく。例えば、3人くらいのチームを率いるようなリーダーの仕事をしていたいとか、機械学習のスキルを活かせる仕事をしていたいとか、フロントエンドよりの仕事をしていたいとか、そのようなものがありえるだろう。

やりたいことがはっきりしない場合、次は「最近自分が課題に感じていることあるか」という質問をし、メンティーが今感じている課題を明確にする。例えば最近自分の成長が鈍化しているのような自分に対する課題や、社内でデータベースの知識があまり共有されていないのような周りを見た時の課題などがありえるだろう。

課題もあまりなければ、次は「自分がこの半年でどういうことやりそうか」という質問をし、これからやる予定のことを明確にする。例えば、この半年は新サービスの立ち上げをやりそうとか、パフォーマンスが重要な仕事をしそうとか、そういうものがありえるだろう。


このあたりの質問をしていき、「なりたい姿・やりたいこと・課題・これからやること」のどれかが明確になれば、それをひとまずのゴールにできる。ゴールの例としては

  • なりたい姿 : 2~3年後にこの人のようになる
  • やりたいこと : 1年後にこの仕事をやっている
  • 課題 : 半年後にこの課題が解決している
  • これからやること : 半年後、○○というタスクをうまくやり遂げている


例えば機械学習の得意なエンジニアと目標設定をするなら

  • 2~3年後にどういうエンジニアになりたいか
    • 専門スキルの機械学習を発揮し続けられるようなエンジニア

のように、ゴールを決められるだろう。


これでひとまず簡単なゴールを決めることが出来た。

現在の自分とゴールのイメージのギャップを考える

ゴールイメージが明確になると、現在の自分との比較ができるようになる。そこで続いては、現在の自分とゴールイメージとのギャップを聞いていく。質問例としては以下のようなものがある。

  • 2~3年後にこの人のようになるために、自分は今どのようなスキルが足りていないのか
  • 1年後にこの仕事をやっているためには、どのようなことが必要か
  • 半年後にこの課題を解決するために、どのようなことが必要か
  • ○○というタスクをうまくやり遂げるために、どのようなスキルが必要か


先程例に挙げた「専門スキルの機械学習を発揮し続けられるようなエンジニアになりたい」というゴールの場合

  • 専門スキルの機械学習を発揮し続けられるようなエンジニアになるために、どのようなスキルが足りていないか
    • 機械学習のスキルを発揮するには、まずベースとなるアプリケーション開発技術が必要
    • しかし、まだサーバーサイドからフロントエンドまで一貫して自分一人で開発はできていない
    • これまでサーバーサイドは行えているが、フロントエンドの経験はほぼない
    • そこで、フロントエンドもある程度できるようにする必要があるのではないか

のように会話していくと、「一機能をサーバーサイドからフロントエンドまで一貫して自分で開発できるスキルがない。特にフロントエンド。」といったギャップが見えてくる。


これでゴールと現在位置とのギャップを明確にできた。

ギャップを埋める目標を考え、アクションを定める

ゴールとギャップが明確になったので、あとはゴールに必要なギャップのうち一つ(もしくは複数)を選び、目標とそれを達成するためのアクションに落とし込むと良い。

僕は基本的に、今後その人が自分で考えていろんなアクションを取れるように、目標の方は柔軟に捉えられるようにしている。逆にアクションの方はかなり具体的になるように、「SMART」の法則で作っている。「SMART」というのは、具体的(Specific)で、計測できて(Measurable)、達成可能で(Achievable)、現実的で(Realistic)、期限が明確である(Timed)というものだ。


例えば先程の、「一機能をサーバーサイドからフロントエンドまで一貫して自分で開発できるスキルがない。特にフロントエンド。」というギャップがあるなら、例えば以下のような目標とアクションを作ることができるだろう。

  • 目標: 半年後までにサーバーサイドからフロントエンドまで含めて一機能を作る経験をする
  • アクション1: 1か月後までに、使っているJSフレームワークのAngularJSの知識を付けるため、それについて書かれた書籍を最低1冊読む
  • アクション2: 2か月後までにAngularJSを使ってTODOアプリのような簡単なアプリケーションを一つ作る
  • アクション3: 上司にフロントエンドの技術を学んだことを伝え、大きめな一機能を上から下まで担当させてもらえるよう提案する
  • アクション4: 半年後までに、任された一機能をやり遂げる


これで目標と、それに対応するアクションプランを決めることが出来た。

まとめ

今回は、自分がエンジニアの目標設定の面談をどのように進めているかについて具体的にまとめてみた。もちろん場合場合によってやり方は変えるのだけど、基本的には今回のような考え方でやっている。他にもやり方があれば教えてもらえればと思う。

「事業成長にコミットするエンジニア組織への道のり」スライドのメモ

非常に良かった記事とかを見た時、これまではEvernoteにメモを残していたのだけど、よく考えたら別にブログに公開しておけば良いよなと思ったので公開してみる。

今回は「事業成長にコミットするエンジニア組織への道のり」というのが良かったのでメモ。

エンジニアを社内に構える意味は

  • 売上最大化あるいは利益最大化させること
  • 事業成長に負けないように技術的な挑戦をすること

これを見て、確かに技術グループの役割は技術で売上や利益を最大化しつつ、成長する事業をさらに成長させるための技術力を着ける責任を持つということだなーと思った。

事業会社におけるエンジニア像

  • 技術で課題を解決する人
  • プロダクト志向で開発をする人

エンジニアにとっても技術は手段と考え、課題解決が出来ているかと問い続けなければならない。

スプリンタータイプとマラソンタイプ

  • スプリンタータイプ: フロント寄り。高速開発やCVR寄与を目的
  • ラソンタイプ: バックエンド、インフラ寄り。高難易度案件で価値を発揮する

エンジニアの風林火山とかあったけど、この区分も面白い。僕はマラソンタイプ。

短期成長のために中期視点が抜けた場合、導くのはエンジニアの役割

事業が成長しているのに、技術的に成長していなかったときにどこかでつまづく話。短期成長と中期視点という考え方で技術的負債とか言語選択とかそういうタスクを考えていきたい。

組織マネジメントとはひとりひとりが輝ける場所を定義すること
すべてがゴール = 多様性を許容

むっちゃ良い。事業の課題と個人のやりたいことをうまくすり合わせられるように心がけたい

中置記法の数式文字列から計算結果を求める操車場アルゴリズム

最近 Algorithms, Part I | Coursera の社内勉強会を開いている。そこで「Stacks and Queues」という章をみんなで学習し、議論したところ、中置記法の数式文字列から計算結果を求める操車場アルゴリズム(Shunting-yard algorithm)というものがあるということを知った。

操車場アルゴリズムというのは、中置記法の数式文字列から計算結果を求めるというようなことができるアルゴリズム。オペレータスタックと値スタックの二つがあるだけでトークンを一つずつ読みつつその都度計算するだけで計算結果を求めることができる。パースして構文木を作るというフローを取ることなく計算ができるというのが面白い。このアルゴリズムの説明は Wikipedia操車場アルゴリズム - Wikipedia の説明が非常に分かりやすい。

このアルゴリズムを使うことで、例えば以下のような数式を計算する計算機を作ることができる。

  • 1 + 2
  • 1 / 2 + 2 * 3 - 1
  • 1.8 * 3 - 2.1 + 0.5
  • 1.8 * ( 3 - 2.1 ) + 0.5
  • ( 10 + 2.2 * 5.5 / 2.2 * ( 1.2 - 0.6 * 3.0 ) / 2 + 4.5 * ( 2.0 - 3 ) )

このアルゴリズム面白いなーと思い、実際に理解を深めるために、この操車場アルゴリズムを実装してみた。内容をブログに書いておく。

今回実装した仕様

操車場アルゴリズムの全てを実装しようとすると難しいので、今回は簡易版で実装してみた。実装した機能は次のとおり。

  • 数値にはdoubleの数値が使える
  • +, -, *, /の演算子が利用できる
  • 括弧を使うことができる

もっと作り込めば右結合性の演算子(例えば^)や関数(例えばsqrt)にもこのアルゴリズムで対応できる。

アルゴリズムの流れ

基本的には数値や演算子、括弧といったトークンを一つずつ読み取り、読み取ったトークンに従ってそれぞれ次のように処理をするだけで良い。

  • tokenが数値なら値スタックに積む
  • tokenがオペレータ(o1)の時
    • オペレータスタックのトップにオペレータo2 があり、o1 が左結合性で、かつ優先順位が o2 と等しいか低い場合、以下を繰り返す
        • o2 をオペレータスタックからpopし、値スタックから値を二つpopし、演算し、結果を値スタックにpushする
    • o1 をオペレータスタックにプッシュする。
    • 注) 右結合性のオペレータは未実装
  • トークンが左括弧の場合、オペレータスタックにプッシュする
  • トークンが右括弧の場合
    • オペレータスタックのトップにあるトークンが左括弧になるまで、オペレータスタックからオペレータをpopし、値スタックから値を二つpopし、それらを演算し、値スタックに結果をプッシュする
    • 左括弧があったらスタックからpopし、何もせずに捨てる

全てのトークンの読み込みが終わったら、最後の処理として

  • オペレータスタックが空になるまで、オペレータスタックからオペレータをpopし、値スタックから値を二つpopし、それらを演算し、値スタックに結果をプッシュする
  • 値スタックに入っている値を返す

この流れで計算式の文字列から計算をして、結果を返すということができる。状態はスタック二つしかないのでそこまで複雑になっていない。

アルゴリズムの実装

アルゴリズムの流れが分かったので、次は実装してみる。上の流れは明確なアルゴリズムとなっているので、そのまま実装すれば良い。

今回はJavaで実装してみた。コード上にコメントも書いているのでコードを読めばスムーズに理解できると思う。

https://github.com/shibayu36/algorithms/blob/master/src/main/java/org/shibayu36/algorithms/FormulaCalculator.java

package org.shibayu36.algorithms;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import org.apache.commons.lang3.math.NumberUtils;

public class FormulaCalculator {
  // オペレーターの優先順位
  private static final Map<String, Integer> opPriority = new HashMap<String, Integer>() {
    {
      put("+", 2);
      put("-", 2);
      put("*", 3);
      put("/", 3);
    }
  };

  /**
   * 数式の文字列を与えると計算結果を返す
   * @param formula 数式の文字列。トークンは空白で区切られている必要がある
   * @return
   */
  public static double calculate(String formula) {
    Stack<String> ops = new Stack<>(); // オペレーターのスタック
    Stack<Double> vals = new Stack<>(); // 値のスタック

    String[] tokens = formula.split(" ");

    for (String token: tokens) {
      if (NumberUtils.isCreatable(token)) {
        // tokenが数値なら値スタックに積む
        vals.push(Double.parseDouble(token));
      }
      else if (isOperator(token)) {
        // tokenがオペレータ(o1)の時
        // - オペレータスタックのトップにオペレータo2 があり、o1 が左結合性で、
        //   かつ優先順位が o2 と等しいか低い場合、以下を繰り返す
        //     - o2 をオペレータスタックからpopし、値スタックから値を二つpopし、
        //       演算し、結果を値スタックにpushする
        // - o1 をオペレータスタックにプッシュする。
        // (右結合性のオペレータは未実装)
        while (ops.size() > 0) {
          String lastOp = ops.pop();
          // "("が入っている場合があるため
          // オペレータスタックの最後もオペレータであることを確認する
          if (isOperator(lastOp) && getOpPriority(token) <= getOpPriority(lastOp)) {
            double val2 = vals.pop();
            double val1 = vals.pop();
            vals.push(applyOperator(lastOp, val1, val2));
          }
          else {
            ops.push(lastOp);
            break;
          }
        }
        ops.push(token);
      }
      else if (token.equals("(")) {
        // トークンが左括弧の場合、オペレータスタックにプッシュする
        ops.push(token);
      }
      else if (token.equals(")")) {
        // トークンが右括弧の場合
        // - オペレータスタックのトップにあるトークンが左括弧になるまで、
        //   オペレータスタックからオペレータをpopし、値スタックから値を二つpopし
        //   それらを演算し、値スタックに結果をプッシュする
        // - 左括弧をスタックからpopするが、何もせずに捨てる
        while (ops.size() > 0) {
          String op = ops.pop();
          if (op.equals("(")) {
            break;
          }
          else {
            double val2 = vals.pop();
            double val1 = vals.pop();
            vals.push(applyOperator(op, val1, val2));
          }
        }
      }
    }

    // 読み取るべきトークンが無くなったら、オペレータスタックが空になるまで
    // オペレータスタックからオペレータをpopし、値スタックから値を二つpopし
    // それらを演算し、値スタックに結果をプッシュする
    while (ops.size() > 0) {
      String op = ops.pop();
      if (isOperator(op)) {
        double val2 = vals.pop();
        double val1 = vals.pop();
        vals.push(applyOperator(op, val1, val2));
      }
    }

    // 値スタックに最後に入っている結果が演算結果である
    return vals.pop();
  }

  private static double applyOperator(String op, double val1, double val2) {
    BigDecimal b1 = new BigDecimal(String.valueOf(val1));
    BigDecimal b2 = new BigDecimal(String.valueOf(val2));
    switch (op) {
      case "+":
        return b1.add(b2).doubleValue();
      case "-":
        return b1.subtract(b2).doubleValue();
      case "*":
        return b1.multiply(b2).doubleValue();
      case "/":
        return b1.divide(b2).doubleValue();
    }

    // ここまで来たら意図しないoperatorが来たということなので例外
    throw new RuntimeException("Unexpected operator: " + op);
  }

  private static int getOpPriority(String token) {
    return opPriority.getOrDefault(token, 0);
  }

  private static boolean isOperator(String token) {
    return opPriority.containsKey(token);
  }
}

さらにテストでうまく動いているか確認する。
https://github.com/shibayu36/algorithms/blob/master/src/test/java/org/shibayu36/algorithms/FormulaCalculatorTest.java

package org.shibayu36.algorithms;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;

public class FormulaCalculatorTest {

  @Test
  public void calculate() throws Exception {
    double result;

    result = FormulaCalculator.calculate("1 + 2");
    assertThat(result).isEqualTo(3);

    result = FormulaCalculator.calculate("1 / 2 + 2 * 3 - 1");
    assertThat(result).isEqualTo(5.5);

    result = FormulaCalculator.calculate("1.8 * 3 - 2.1 + 0.5");
    assertThat(result).isEqualTo(3.8);

    result = FormulaCalculator.calculate("1.8 * ( 3 - 2.1 ) + 0.5");
    assertThat(result).isEqualTo(2.12);

    result = FormulaCalculator.calculate("( 10 + 2.2 * 5.5 / 2.2 * ( 1.2 - 0.6 * 3.0 ) / 2 + 4.5 * ( 2.0 - 3 ) )");
    assertThat(result).isEqualTo(3.85);
  }
}

これでテストが通ったので、操車場アルゴリズムを実装し、中置記法の数式文字列から計算結果を求めることができた。

まとめ

今回は操車場アルゴリズム(Shunting-yard algorithm)というものを実際に実装してみた。単純にスタックを二つ使うだけで、構文木などを作らなくとも、パースしながら同時に計算することで計算結果を得られるというのが面白かった。

実際に実装してみるとアルゴリズムについて理解が深まり、かつ知識が定着するので、今後も面白そうなアルゴリズムがあったら実装してみたい。

【補足】実行しながらスタックの様子を眺めてみる

理解度を深めるために、実行しながらスタックの様子を眺めてみる。部分部分に注釈を書いた。

実装してみるとこんな感じで自由に動かしてさらに理解が深められるので良いですね。

( 10 + 2.2 * 5.5 / 2.2 * ( 1.2 - 0.6 * 3.0 ) / 2 + 4.5 * ( 2.0 - 3 ) )

formula: ( 10 + 2.2 * 5.5 / 2.2 * ( 1.2 - 0.6 * 3.0 ) / 2 + 4.5 * ( 2.0 - 3 ) )
===============
token: (
ops: [(]
vals: []
------------
token: 10
ops: [(]
vals: [10.0]
------------
token: +
ops: [(, +]
vals: [10.0]
------------
token: 2.2
ops: [(, +]
vals: [10.0, 2.2]
------------
token: *
ops: [(, +, *]
vals: [10.0, 2.2]
------------
token: 5.5
ops: [(, +, *]
vals: [10.0, 2.2, 5.5]
------------
# *と/の優先度が一緒なので*を計算して/をpush
token: /
ops: [(, +, /]
vals: [10.0, 12.1]
------------
token: 2.2
ops: [(, +, /]
vals: [10.0, 12.1, 2.2]
------------
# /と*の優先度が一緒なので、/を計算して*をpush
token: *
ops: [(, +, *]
vals: [10.0, 5.5]
------------
token: (
ops: [(, +, *, (]
vals: [10.0, 5.5]
------------
token: 1.2
ops: [(, +, *, (]
vals: [10.0, 5.5, 1.2]
------------
token: -
ops: [(, +, *, (, -]
vals: [10.0, 5.5, 1.2]
------------
token: 0.6
ops: [(, +, *, (, -]
vals: [10.0, 5.5, 1.2, 0.6]
------------
token: *
ops: [(, +, *, (, -, *]
vals: [10.0, 5.5, 1.2, 0.6]
------------
token: 3.0
ops: [(, +, *, (, -, *]
vals: [10.0, 5.5, 1.2, 0.6, 3.0]
------------
# )があったので、最初に(が見つかるまで、つまり*と-を計算して値をpush
token: )
ops: [(, +, *]
vals: [10.0, 5.5, -0.6]
------------
token: /
ops: [(, +, /]
vals: [10.0, -3.3]
------------
token: 2
ops: [(, +, /]
vals: [10.0, -3.3, 2.0]
------------
# /より+のほうが優先度が低いので、/を計算して+をpush
token: +
ops: [(, +]
vals: [8.35]
------------
token: 4.5
ops: [(, +]
vals: [8.35, 4.5]
------------
token: *
ops: [(, +, *]
vals: [8.35, 4.5]
------------
token: (
ops: [(, +, *, (]
vals: [8.35, 4.5]
------------
token: 2.0
ops: [(, +, *, (]
vals: [8.35, 4.5, 2.0]
------------
token: -
ops: [(, +, *, (, -]
vals: [8.35, 4.5, 2.0]
------------
token: 3
ops: [(, +, *, (, -]
vals: [8.35, 4.5, 2.0, 3.0]
------------
token: )
ops: [(, +, *]
vals: [8.35, 4.5, -1.0]
------------
token: )
ops: []
vals: [3.85]
------------