@ledsun blog

無味の味は佳境に入らざればすなわち知れず

RSpecでSharedExamplesを使ったときのエラー表示の現状

RSpecで、SharedExamplesを使うと、失敗したテストの行番号の代わりに [1:2:2:3:1] みたいなのが表示されて、どのテストを見ればいいのかわかりにくくなるのが辛いです。 2015年からInclude line numbers as well as index in output · Issue #2119 · rspec/rspec-coreがあります。

https://github.com/rspec/rspec-core/issues/2119#issuecomment-173664887 の辺りを ざっくり要約すると

SharedExampleを使ったテストが失敗した時に、見たい場所はspecを定義している場所とexampleを定義している場所の2箇所ある。 どっちを見たいかは人によって変わる。 specを定義している場所を表示すると、それはそれで不便。 Summary output of shared failed examples is useless when examples defined in external file · Issue #793 · rspec/rspec-coreで議論済み。 かと、言って両方を表示するのはSummaryとしては冗長すぎて、それも不便でしょ?

今週の作業メモ

JavaScripting

runコマンドのバグ

https://github.com/workshopper/javascripting/issues/218 javascripting run hogohoge.jsを実行するとたしかに[object Object]が表示されます。 javascriptingではrunコマンドは使っていません。

https://github.com/workshopper/workshopper-adventure/blob/master/index.js#L264 workshopper-adventure 経由です。 アンドキュメントなコマンド発見して実行して上手く動かないと言われるのは心外だが、不親切なのも確か。 こういうときはどうするのがいいのかなあ? コマンドをworkshopperに投げる前に検査してUsage表示するとか?

helpコマンドでrunコマンドでの説明が出てくるのか・・・全然アンドキュメントじゃないな

~ bin/javascripting help

 # JAVASCRIPTING

 ## javascripting のワークショップに対して質問がありますか?

  エキスパートチームはあなたの質問を待っています。あなたもNode.jsのマスター
  になれるようにここに質問(New Issue)をしてみてください:

     https://github.com/nodeschool/discussions/issues

  どんな質問でもどうぞ!

  一般的なNode.jsのヘルプが必要であればNode.jsの日本のGoogleグループがありま
  す:

     https://groups.google.com/forum/#!forum/nodejs_jp

 ## javascriptingのバグが見つかった場合または貢献したい場合:

  javascriptingのリポジトリはここにあります:

     https://github.com/sethvincent/javascripting.git

  バグの報告やPullリクエストは日本語でいつでもどうぞ!

 ## 使い方

  javascripting .....................
  ワークショップを選択する対話的メニューを表示します。

  javascripting list ................
  登録されているすべてのワークショップの名称一覧を表示します。

  javascripting select NAME ......... ワークショップを選択します。

  javascripting current .............
  現在選択されているワークショップの名称を表示します。

  javascripting print ...............
  現在選択されているワークショップのガイドを表示します。

  javascripting next ................
  現在選択されているワークショップの次の未修了のワークショップのガイドを表示
  します。

  javascripting reset ...............
  ワークショップの進捗状況をリセットします。

  javascripting run program.js ......
  あなたが作成したプログラムを実行します。

  javascripting verify program.js ...
  あなたが作成したプログラムが正しいかを検証します。

  javascripting -l <language> .......
  システムで使用する言語を指定されたものに変更します。

ソースコードリーディング

javascriptingソースコードからjavascriptingworkshopperの役割分担を読みときます。 https://github.com/workshopper/javascripting/blob/master/index.jsworkshopper-adventureインスタンスを読み込んでいます。 サブコマンドによる分岐の全体的な制御フローは workshopper-adventure が担当します。

https://github.com/workshopper/javascripting/blob/master/index.js#L8 で、 menu.json に含まれる problem の一覧を読む。

https://github.com/workshopper/javascripting/blob/master/index.js#L12-L14problemごとに require('problems/introduction') な処理をする関数を作る。

https://github.com/workshopper/javascripting/blob/master/problems/introduction/index.jsrequire("../../lib/problem")(__dirname) なので 実質的に https://github.com/workshopper/javascripting/blob/master/lib/problem.js を実行している。

createProblem 関数で作った exercise を引数にして https://github.com/workshopper/workshopper-adventure/blob/master/index.js#L345 が呼ばれます。

この辺は VSCodeデバッグしながら追いかけています。 https://code.visualstudio.com/docs/nodejs/nodejs-debugging

{
  // IntelliSense を使用して利用可能な属性を学べます。
  // 既存の属性の説明をホバーして表示します。
  // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/bin/javascripting",
      "args": ["verify", "index.js"]
    }
  ]
}

修正案

https://github.com/workshopper/javascripting/blob/master/lib/problem.js ではrunメソッドも呼んでいる。この辺をいじると、クリーンな修正ができるのだろうか?

デバッグするとたしかに run メソッドは呼ばれています。ただし、出力文字列はこで作っているわけでは無さそうです。 https://github.com/workshopper/javascripting/blob/master/lib/problem.js#L53-L55 workshopper-adventure が出力文字列は作っていそう、javascripting 側から上書きする方法があるのかな?

対処

単に run メソッドの定義を消せばよいだけだった。こんなに簡単な話だったとはね、ふふ

~ bin/javascripting run index.js
run is not required for this exercise.

github.com

今週の作業メモ

spring

Rails 5.2.3でrails consoleを実行すると、プロンプトが出る前で止まります。

~ bundle exec rails --version
Rails 5.2.3
~ bundle exec rails c
^CTraceback (most recent call last):
    15: from bin/rails:3:in `<main>'
    14: from bin/rails:3:in `load'
    13: from /Users/shigerunakajima/circular_dependency_detected_job/bin/spring:15:in `<top (required)>'
    12: from /Users/shigerunakajima/circular_dependency_detected_job/bin/spring:15:in `require'
    11: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
    10: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
     9: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
     8: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
     7: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
     6: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:24:in `call'
     5: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
     4: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:35:in `call'
     3: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:42:in `warm_run'
     2: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:62:in `run'
     1: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:117:in `verify_server_version'
/Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:117:in `gets': Interrupt

https://github.com/rails/spring/blob/v2.1.0/lib/spring/client/run.rb#L117 ここのserver(どうもUnixソケットらしい)の読み込みで止まっている。

試しに、Gemfileからspringを消してみると、期待通りにプロンプトが表示されます。

~ bundle exec rails c
Loading development environment (Rails 5.2.3)
irb(main):001:0> 

@p_ck_ 曰く

springが悪さをするのに疲れ果てて最近は DISABLE_SPRING=1 してます 似たような状況のときは bin/spring stopをしたら直っていたので毎回それをしていた記憶があります

Bingo!

bootsnap

ruby 2.7-preview1でRails 6.0.0.rc2の rails --version を実行すると、エラーが起きます。

~ rails --version
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Traceback (most recent call last):
        36: from bin/rails:3:in `<main>'
        35: from bin/rails:3:in `load'
        34: from /Users/shigerunakajima/my_first_rails_6/bin/spring:15:in `<top (required)>'
        33: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        32: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        31: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
        30: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
        29: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
        28: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
        27: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
        26: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `call'
        25: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `load'
        24: from /Users/shigerunakajima/my_first_rails_6/bin/rails:8:in `<top (required)>'
        23: from /Users/shigerunakajima/my_first_rails_6/bin/rails:8:in `require_relative'
        22: from /Users/shigerunakajima/my_first_rails_6/config/boot.rb:4:in `<top (required)>'
        21: from /Users/shigerunakajima/my_first_rails_6/config/boot.rb:4:in `require'
        20: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/setup.rb:30:in `<top (required)>'
        19: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap.rb:30:in `setup'
        18: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache.rb:9:in `setup'
        17: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `require_relative'
        16: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `require'
        15: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:288:in `load_dependency'
        14: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `block in require'
        13: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
        12: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
        11: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
        10: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
         9: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
         8: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache/iseq.rb:1:in `<top (required)>'
         7: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `require'
         6: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:288:in `load_dependency'
         5: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `block in require'
         4: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
         3: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
         2: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
         1: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require': no implicit conversion of String into Integer (TypeError)

エラーが起きているのはこの辺 https://github.com/Shopify/bootsnap/blob/v1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L17-L24

エラーが起きるときの path の値は "bootsnap/bootsnap"です。 require_without_bootsnap は require の alias_methodなので、 require "bootsnap/bootsnap"で再現します。

~ irb
irb(main):001:0> require "bootsnap/bootsnap"
Traceback (most recent call last):
        7: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/bin/irb:23:in `<main>'
        6: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/bin/irb:23:in `load'
        5: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        4: from (irb):1
        3: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:34:in `require'
        2: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:130:in `rescue in require'
        1: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:130:in `require'
TypeError (no implicit conversion of String into Integer)

これでRuby 2.7 + Rails 6ではなくRuby 2.7 + bootsnapの問題だと絞り込めました。bootsnapのissueを漁ると https://github.com/Shopify/bootsnap/issues/258 が 発見できます。 https://github.com/Shopify/bootsnap/issues/258#issuecomment-518726704 をみるとmasterブランチでは修正済みのようです。

https://github.com/Shopify/bootsnap/commit/a163c7cddfccd68277f6ea43b1831378509817c9 パッチの作者は須藤さんや!さすがや、俺の遥かに前を行っているッ!!! Ruby2.7からはRubyのリビジョン番号が数値からgitのSHAハッシュに変わったので、数値前提のソースコードが壊れたらしい。なるほど・・・そういう変更もあるのか。 bootsnap.c なので、bootsnapはC拡張なのねえ。

ていうかBootsnapって、なんなんだろう?名前しか知らないや

READMEによりますと、Rubyプログラムの起動を早くするためのgemです。大きなRailsアプリケーションだと、50%以上の高速化の実績があります。主な手法としては Kernel#require を上書きして、キャッシュファイルをつかうことで、読み込むrubyスクリプトの検索を速くします。 今回エラーが出ていた https://github.com/Shopify/bootsnap/blob/v1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L17-L24 は、まさにこのKernel#require を上書きしている部分!

もう一つの最適化手法にiSeq(コンパイル済みのRubyバイトコード)のキャッシュがあります。MessagePack形式でファイルに保存しておくことで、Ruby本体による「ファイルの読み込み〜コンパイル」を「ファイルの読み込み」だけにできます。iSeqはコンパイルしたRubyのバージョン(だけでなくリビジョン単位)によって変わるので、キャッシュが有効かどうか判断するには、Rubyのリビジョン番号が必要です。このリビジョン番号を取るところがRuby 2.7で動かなく成っていたのの修正が https://github.com/Shopify/bootsnap/commit/a163c7cddfccd68277f6ea43b1831378509817c9 なるほどー!

jQueryUI ダイアログ

jQueryUIダイアログには、閉じた時にその前に選択していた要素を再選択する機能があるようです。

Upon closing a dialog, focus is automatically returned to the element that had focus when the dialog was opened.

https://api.jqueryui.com/dialog/

なるほど・・・

closeするときに this.opener をfocusします。 https://github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L224

this.opnerは開いたときの document.activeElement なのかな? https://github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L273

$.ui.safeActiveElement は、IEのエラーを出なくした document.activeElement 取得関数です。 https://github.com/jquery/jquery-ui/blob/74f8a0ac952f6f45f773312292baef1c26d81300/ui/safe-active-element.js#L12

タスクの完了からはじめるプロジェクト進捗管理

何の話?

タスクの進捗状況をパーセンテージで管理することがあります。

例えば、進捗報告会議で現在着手中のタスクAの進捗を報告するとき「50%完了しています。」という報告の仕方です。 進捗を「タスクの進捗率」で測る方法には、ちょっとした問題があります。

「予定しているタスクのうち2つ完了しています。残りのタスクは3つです。」というように、完了したタスクの数と未完了のタスクの数で報告すると、プロジェクトの進捗管理が、すこし良くなります。

どんなメリット?

プロジェクトの進捗を「タスクの進捗率で測る」方法に比べて、「完了したタスクの個数を数える」方法は、早く正確な進捗情報が手に入ります。 早く正確な情報が手に入れば、プロジェクトが遅れたときにとれる対応策の選択肢が増えます。

  1. お客さん(ステークホルダー)とスケジュールの調整をする
  2. プロジェクト全体で確保してあるバッファを使う
  3. 予想より早く作業が進んでいる人を、遅れている作業のヘルプに割り当てる

プロジェクトの遅れの発見がおそいと、1のような大きな対応しか取れません。 遅れを早くみつけると、2, 3のような、会社内、チーム内のより小さな対応でカバーできるチャンスが増えます。

遅れを早く見つけ、速く小さな対応をする、プロジェクトマネージメントの腕の見せどころです。

f:id:ledsun:20190514170032p:plain
プロジェクトマネージメントの腕の見せどころ

進捗を進捗率で測るときの落とし穴

なぜ、「タスクの進捗率」で測るより、「完了したタスクの個数」で測る方が、早くタスクの遅れに気づけるのでしょうか?

「タスクの進捗率の報告」には次の性質があります。

進捗率10%のタスクは、それまで掛かった時間の10倍の時間を掛けても完了す(100%にな)るとは限らない

なぜでしょうか?

不確実性コーン

ソフトウェアの見積には不確実性コーンと呼ばれる性質があります*1。下の図をみてください。

不確実性コーン
不確実性コーン
*2

プロジェクトの初期段階では、見積もりが1/4倍〜4倍になる不確実性があります。プロジェクトが進むに連れて見積と実際に掛かる工数(実績)が離れる不確実性が減っていきます。プロジェクト完了時には不確実性が0に収束します。 プロジェクトの初期段階では、細かい設計や実装方針などが決めておらず、決定した設計・実装方針によって必要な工数が上下します。 必要な機能があとから発見され工数が増えることもあります。 プロジェクトが進むうちに、新たに発見される情報は減り、いつしかプロジェクトは収束します。 そんな経験はないでしょうか?

タスクの見積りの不確実性

「ソフトウェア見積り」の中で「不確実性コーン」は、プロジェクト単位の話として挙げられています。 個人的に、「不確実性コーン」はタスクにも適用できると考えています。 あるタスクを進めた時、タスク着手前に考えていた必要な時間と、タスク完了までに実際にかかった時間は、たいてい一致しません。 実際にタスク着手してみて、思っていたより簡単に済むことや、考えてもいなかった問題を発見して予想以上に時間がかかることがあります。

また、作業を進めるにしたがって、タスクの完了に必要なことがだんだんわかっていき、不明な部分が減り、見積と実績の差が減っていく点も一致しています。

報告する進捗率はどうやって決めてる?

タスクに着手した初期段階では、見積りと実際に掛かる時間には大きな乖離があります。 仮に、マコネルの例にように1/4倍〜4倍の不確実性があるとしましょう。 作業者が「タスクを10%進めた」と思っている時、実際に掛かる時間の2.5%しか進んでいない可能性もあれば、40%進んでいる可能性もあります。

10%の時点で40%進んでいる可能性があるのであれば、40%と報告しても嘘ではありません。 プロジェクト管理者が10%と報告したとき不機嫌になり、40%と報告したときご機嫌になるのであれば、プロジェクト管理者を喜ばせたい作業者は40%と報告するするかもしれません。 あるいは、自分がプロジェクト管理者の機嫌に左右されていると感じる作業者は、影響を差っ引いて低目に2.5%と報告するかもしれません。

作業者の実感する進捗率と報告される進捗率の数値は同じとは限りません。

プロジェクト管理者の感情

プロジェクト管理者がポーカーフェイスを貫ければ、作業者の実感通り10%と報告してもらえるかもしれません。 予想より遅い進捗を報告されたプロジェクト管理者は、なんらかの対策を考える必要があるので、不機嫌にならないとしても困りはするでしょう。 おそらく困っていることは顔に出るでしょう。

プロジェクトのあらゆる場面で感情を隠しつづけるのは難しいです。 プロジェクトが順調な時はいいです。 プロジェクトが遅れ、遅れに対して上司やお客様から苦情をもらっているときに、作業者からもっと遅れる報告をもらったら、感情的にならないことは難しいでしょう。

プロジェクト管理者が望んでも望まなくても、作業者に善意があっても悪意があっても、作業者はプロジェクト管理者のなんらかの意図を読み取って、都合の良い進捗率を勝手に報告します。

f:id:ledsun:20190514170215p:plain
プロジェクト管理者の機嫌を伺って進捗報告する作業者

90%まで進行して完了しないタスク

プロジェクト管理あるあるに「永遠の進捗率90%」があります。 あるタスクの進捗率が順調に推移して90%まで進んで、そこから進まなくなる現象です。

「不確実性コーン」は、直接的にはタスクの初期段階で見積と実績の乖離を生みます。 しかし、本来幅があるはずの進捗率を一つの数値に落とし込んで報告する作業は、間接的に、タスクの末期になってもその威力を発揮します。

特に、作業者が、自身で問題を解決したいと考えている場合に起こります。 あるいは、プロジェクト管理者が、作業者だけで遅れを解消させようとする場合に起こります。

進捗率では進捗率はわからない

以上のように「報告するタスクの進捗率」は作業者の個人の価値観や、プロジェクト管理者と作業者の関係によって変ります。 「進捗率の報告」では、進捗率はわかりません。

作業者のトレーニングや、作業者とプロジェクト管理者の関係構築によって、「進捗率」の認識を合わせて行くこともできるでしょう。 人数が多いと修正する「作業者の振る舞い」と「作業者とプロジェクト管理者の関係」は増えます。

もっと簡単な方法があります。 プロジェクトの進捗を「完了したタスクの個数を数え」て測りましょう。 「完了したタスクの数」は、プロジェクト管理者と作業者の間の誤解が少ないです。 良くも悪くも報告内容を誤魔化せません。

どうやって始める?

進捗を「完了したタスクの個数を数え」で測るためにはタスクのサイズを小さくします。

進捗報告のタイミングで、進行中のタスクが「完了したタスク」と「未完了のタスク」の半々になるようにしましょう。 割合は重要でありません。 進捗報告の際に「完了したタスク」と「未完了のタスク」が混ざることが重要です。

具体的には、タスクのサイズを「次の進捗報告までに、理想的な見積で完了する」サイズより小さくしましょう。 毎日進捗報告するのであれば、1日より小さくします。 毎週であれば5日より小さくします。

実績が見積を下回ったタスクは「完了したタスク」になります。 実績が見積を上回ったタスクは「未完了のタスク」になります。 次の進捗報告では「完了したタスク」と「未完了のタスク」が混ざるはずです。

「ごまかしのないプロジェクトの進捗率」が、「進捗報告のたびに」得られます🎉

直近のタスクだけを小さく見積る

プロジェクトのすべてのタスクを1日単位のタスクに分割するのは大変です。 「完了したタスクの個数を数える」のに必要な小さなサイズに見積もるのは、直近のタスクだけで十分です。

見積はプロジェクト全体の「不確実性コーン」の影響を受けます。 プロジェクトの初期に全てのタスクを見積もっても実績と乖離する確率が高いです。 不確実性が高いうちに多くのタスクを細かく見積もっても、労多くして功少なしです。 プロジェクトが進んでから、細かく見積もれば、不確実性が低くなった段階で見積もれるので、見積の精度が上がります。

細かく見積もるのは、直近の進捗報告 1サイクルか2サイクル分のタスクにしましょう。 直近でない、大きな単位のタスクは、荒く見積もって、プロジェクトの不確実性に応じて2〜4倍のバッファを積みましょう。

タスクの分け方

タスクを小さいサイズに分けるときにはコツがあります。

大きなタスクには、不明な部分が多く入っています。 不明な部分を明確する作業をタスクとして切り出しましょう。

  • 仕様の不明点を顧客(またはプロダクトマネージャー)にインタビューして解消する
  • UXの不明点を解消するためにデモ画面を作る
  • 使用ライブラリの不明点をサンプルプログラムを作って解消する

不明点が少ない作業は、やることがわかっています。 分担しやすい部分で、2つなり3つなりのタスクに分ければよいでしょう。

f:id:ledsun:20190515163031p:plain
わからないことをわかるようにする作業をタスクにしよう

どこから始める?

個人プロジェクト

一人プロジェクトの場合、タスクの進捗率を意識することはないと思います。

例えば、卒論の進捗率をページ数で測ることはあっても、「実験結果の考察が30%進んだ」と管理することはないと思います。 それは30%と数値化しても、3.3倍の時間で完了するとは限らないからです。 タスクの進捗は「全然わかっていない」や「あともうちょっと」で把握していれば十分です。

「タスクの進捗率」を求めるのは、複数人の作業進捗を集計できるように正規化するためです。 「Aさんが40%進んでいてBさんが50%進んでいるので、全体としては平均して50%」と全体像を把握するためです。

個人プロジェクトに「タスクの進捗率」は必要ありません。

少人数プロジェクト

3〜5人程度のプロジェクトを考えてみましょう。 進捗を「完了したタスクの個数を数え」て測るのを、始めやすい規模です。

人間には、タスクを分けるのが上手い人と下手な人がいます。 上手い人には、必要なタスクのサイズを伝えれば、それで十分です。 下手な人は、下手なので、タスクを分ける手伝いをしてあげましょう。

タスクを分けるのが下手な人の多くは、タスクの中の「何がわからないのかわからない」ことが多いです。 地道に「わかっていること」と「わかっていないこと」を聞いて、わかっていないことを「不明な部分を明確する作業」としてタスクに切り出しましょう。 手間はかかります。

タスクの進捗率で報告させれば、タスクを分けるのが下手な人でも、とりあえず作業に着手して進捗報告ができます。 たとえば「設計資料のファイルを開いたから10%」、「hoge.cファイルを作ったから10%」。 「何がわからないのかわからない」人でも、わかることははあります。わかることの作業ができます。 作業さえすれば進捗率は増やせます。90%まで。

プロジェクト管理者としてみたら、作業者のスキルが多少不安だと思っていても、進捗率が増えている間は安心できます。 でも、なぜか90%まで進んだら止まります。タスクはなかなか完了しません。 90%ということはスケジュール上は後半のはずです、リカバリ策が取りにくい段階で、遅れていることが判明します。 結局困りますよね? 最初から、タスクを分ける作業を手伝っておけばよかったです😥

もっと悪くすると、その作業者は複数のタスクを担当しているかもしれません。 10個のタスクが90%まで進むんです。 プロジェクトの進捗率は90%です。 その実態は・・・怖いですね、恐ろしいですね。さよなら、さよなら👋

大規模プロジェクト

20人程度のプロジェクトを考えてみましょう。 プロジェクトの参加人数が多いと、タスク分割が下手な人も増えます。

プロジェクト管理者が、タスク分割が下手な人全員の「タスクを分ける作業」を手伝うのは不可能です。 頑張って「タスク分割できる人」を集めましょう。 少なくとも参加人数の1/4ぐらいタスク分割できる人を集めて、残りの人の「タスク分割のお手伝い」は「タスク分割できる人」達に見てもらいましょう。

「タスク分割のお手伝い」の工数は余計に掛かります。 全体の見積に、タスク分割の工数も入れておきましょう。 「プロジェクト管理工数」というやつです。

f:id:ledsun:20190515163636p:plain
タスク分割が得意な人にチームを率いてもらおう

*1: Cone of Uncertainty - Wikipedia によると「不確実性コーン」の概念は1958年の化学業界には既にあり、1981年にベームがソフトウエア産業に持ち込み、1997年にマコネルが「不確実性コーン」という呼び名にしたそうです

*2:この図はプロジェクトの本質とはなにか | 日経 xTECH(クロステック) から引用しました。もともとは「ソフトウェア見積り」(スティーブ・マコネル/日経BPソフトプレス)に掲載されている図です。

Ruby2.6 に関数合成オペレータが追加されたのでFizzBuzzで遊ぶ その2

ledsun.hatenablog.com

の続きです。 もうちょっと関数合成しがいのある書き方を思いつきました。

fizz = -> x {[x, ('Fizz' if x % 3 == 0)]}
buzz = -> (x) {[x[0], x[0] % 5 == 0 ? "#{x[1]}Buzz" : x[1]]}
filter = -> (x) {x[1] || x[0]}
do_fizzbuzz = filter << buzz << fizz

(1..15).each {|n| p do_fizzbuzz.call(n)}

fizzに変換する関数とbuzzに変換する関数を組み合わせるとfizzbuzzになるのは、ちょっと関数を合成している感があります。

RubyKaigiに行く意味とは?

結論をいうと「RubyKaigiに行くと自分が世界最高のエンジニアになる可能性」が得られます。どういうことでしょうか?

目的地を知らないとどうがんばっても目的地にたどりつけません。当たり前の話です。エンジニアとしての成長も一緒です。自分がエンジニアとして成長する上限は「自分の心の中にある理想のエンジニア像」で決まります。自分がどうなりたいか知らないと、それ以上には成長できません。

普通に会社に来て仕事をしていると「自分の心の中にある理想のエンジニア像」は先輩社員です。それ以外の人と会わなければ、知っている人の中から自分に一番合いそうな人を選ぶのが自然です。社内の一番優秀なエンジニアは、日本最高のエンジニアかというと、残念ながらそうではありません。だから、自分の心中にある理想のエンジニア像は、日本全体でみると、「中の上」や「上の下」のエンジニアになります。

さっき言ったように「自分のエンジニアのとして成長する上限は、自分の心の中にある理想のエンジニア像」で決まります。真面目に仕事して、真面目に自習しても、成長の上限は「中の上」か「上の下」のエンジニアです。経験年数によりません。5年やろうが10年やろうが、上限以上に成長できません。

もっとすごいエンジニアに成長するにはどうしたらよいでしょうか?「自分の心の中にある理想のエンジニア像」のレベルを上げます。そのためには何をすればいいでしょうか? 最高のエンジニアに出会って交流すればいいのです。そうすれば「自分の心の中にある理想のエンジニア像」が出会った最高のエンジニアになります。

で、RubyKagiがどういうカンファレンスかというと、世界最高のRubyエンジニアが集まるカンファレンスです。だから、RubyKaigiに参加して、勇気を出して最高のエンジニアに話しかければ「自分の心の中にある理想のエンジニア像」が世界最高のエンジニアになります。

というわけで結論。 RubyKaigiに行くと自分が世界最高のエンジニアになる可能性が得られます。 参加しても必ず「世界最高のエンジニア」になれるわけではありません。 でも、参加すると、その可能性は0でなくなります。

SIerでのプログラマ教育

現状は次の記事に非常にリアルに書かれていると思います。

www.megamouth.info

現状わかったから、じゃあ、どうしようか?という話を書きます。

今回扱わない話

適性ミスマッチについては扱いません。 新卒でIT企業に開発職で入ってみたが、プログラマが向いていないと気づいたときの話です。 これはメンバーシップ型雇用の欠点なので、1企業がどうこうする話ではないので、扱いません。

特に規模の小さな会社では、他の職種への転換が難しいです。 かといって、今すぐジョブ型雇用に転換できるかといえば、新卒の職業訓練する機関が足りてないので難しそうです。

DIVE INTO CODE | プログラミングスクール のような企業が増え、競争によって洗練されると違うのかもしれません*1

社内教育

ブートキャンプ

現在では、私が新卒であった2002年頃に比べ、ブートキャンプ的な新入社員研修の効率はかなり上げれる社会情勢になってきました。

常駐を要求されない案件が増えてきました。 これにより開発チームは社内で働くことができるようになりました。 現役エンジニアが社内で働いていれば、現役エンジニアの労働時間の一部を研修に当てることができます。

エンジニアのが常駐していると、研修に参加するには案件から抜ける必要があります。 そうすると研修に参加するエンジニアの売上が100%減になるため、研修にかかるコストが大きくなります。 そのため研修の教師役には、何らかの理由で現場から引退したエンジニアが当たることが多かったです。 教師役の知識が制約になって、現場で使っている技術を題材できないことがありました。

次に、今は社会人プログラミングスクールが出てきたため、未経験を新卒採用する必要がなくなりました。 新卒採用の問題点は、入社時期が年間で特定の一日に固定されることです。 これが制約となって、研修スケジュールが決まります。 つまり4月〜5月に多くの新人をまとめて研修する必要があります。 複数の教師役を一時期に投入する必要があります。

新卒採用をやめれば、未経験を通年採用できます。 さらに一人ずつ採用することができます。 すると研修内容を特定の一人に対して設計することができます。 研修を複数人に同時に実施すると、どうしても進みが遅い人に合わせて進める必要があります。 研修効率が良くない人が出てきます。 一人ずつ採用すれば、この効率を上げることができます。

常駐案件をやめ、未経験者を通年採用すると、複数人の現役エンジニアが分担して一人の未経験者を教育することができます。 これは15年前の新卒採用一括研修と比較すると、大きなアドバンテージです。

OJT

一方でOJTに関しては、未だに大きな問題があります。

受託開発にて、教育をプロジェクトマネージャーの責務にするのは無理があります。 プロジェクトの利益と教育のコストは矛盾しています。 プロジェクトの利益を上げるには、教育に掛かるコストを削って

ベテランプログラマでも困るような雑な投げ方で案件を振って、出来たらヨシヨシ、出来なかったら、ハイハイとばかりに全部書き直して納品

するほうが効率が良いのです。

それでも最大限の善意で教育はします。しますが、掛けられるコストの上限はプロジェクトの利益です。 その結果

話したりハンズオンしている時に、あっこの子、変数のことわかってないな、と感じたら、ホワイトボードを持ち出してきて、例の"x"と書いた箱の絵に矢印を引いて、値が入っている図を書いて、「わかった?」「あ、はい」みたいなやり取りをして終わり、という程度の「教育」

になります。

これを打破するには、プロジェクト外の人が教育する必要があると思っています。 そのためにも常駐しない案件は大事です*2。 具体的な施策は模索中です。

プログラミング教育の難しさ

ここからは愚痴っぽい話です。興味がない人は、この辺でお帰りください。

プログラミング教育

教育は難しいです。 私の教育のイメージは「100mの壁へのチャレンジを、1mずつにわけて100回のチャレンジに変える」ことです。 教える側の教育スキルが不足していると、チャレンジの分割が上手くできず、100mの壁を見せてそのまま挑戦させます。

ベテランプログラマでも困るような雑な投げ方で案件を振って

です。多少上手くなっても、50mずつ2つにわけれるとか、せいぜい10mずつの10個にわけれる程度です。

現時点では社会全体で、プログラミング教育のノウハウが蓄積されていません。 おそらく誰も100分割することはできていないのでしょう。 長い伝統があり教えるプロセスが細かく分割されている分野は小学校教育です。 例えば、小学校の算数の授業時間は1011時間*3です。 126営業日です。小学校算数を理解するだけでも、半年ぐらいは掛かるわけです。 それより遥かに複雑なプログラミングの入門が1〜2ヶ月の社内研修でできるわけないのです。

というわけでOJT、さらに主に個人の自習に頼っているのが、現状のプログラミング教育です。 そしてOJTだけで生産されるのは

この術式から得られる人材というのは、どんな要件に対しても、ググり&コピペで向かい撃つ、素手で熊を殴り続ける、よくわからないグラップラーのような存在

です。

プログラムに関するすごい知見を有した向上心の塊みたいな俊英

になるのは、自習したやつだけです。

インプットとアウトプット

ところが、自習できるには自習のスキルが必要です。 インプットとアウトプットです。

を読んでも、インプットとアウトプットができることが前提です。

インプットは、要するに、日本語が読めて、論理が理解できて、応用できることです。 アウトプットは日本語が書けて、その日本語の論理が通っていて、検証できることです。

アウトプットの訓練はやろうと思えばできます。文章を書かせて添削すればいいのです。 コードレビューとそんなに変りません。ソースコードのイディオムが日本語のイディオムに変わるだけです。 議事録の添削であるとか、読書レポートの添削であるとかやればなんとかなります。 多くの場合は、「喋る日本語」と「書く日本語」が違うことを知らずに「喋る日本語」をそのまま書いていることが原因です。 「書く日本語」のテクニックを教えれば、日本語が書けるようになります。 作業量としては、対象者の元のスキルに依存しますが、A4 2枚のレポートを10回ぐらい書き直させれば、それなりの日本語になります。 お互い時間は掛かりますが、やろうと思えばできます*4

インプットの訓練は正直わかりません。 インプットはアウトプットと違い目に見えません。 何に躓いているのか見えません。 文法的に読み取れるかどうかの訓練はできると思います。 おそらく問題はそこではありません。

本の題材と自分の経験がミスマッチだった時に、まったく何も読み取れなくなる人がいます。 私は、そういう場合に、話の抽象度を上げてコンテキストを一致させます。 例えば

を読んだ時に、工場長をやった経験も無ければ奥さんに出ていかれた経験はないので、そのまま役に立つノウハウを得ることはできません。 それでも「ボトルネックを探してそれを中心に最適化していく」概念は理解できます。 そこから「ソフトウェア開発プロセスボトルネックは何か?」という問いを得ることができます。

これができない人がいます。 できないということは未経験の内容の本が読めません。つまり新しい技術を本から学ぶことができません。 この人達は、本を読む時に、その文言をなんのアレンジもせずに、ただひたすら文を暗記しようとするようです。 想像では、娯楽として本を読んだ経験がなく、読んだことある本の90%ぐらいが教科書と参考書だったのではないかと思っています。 そのため「読書とは本の内容を暗記すること」で「想像のために刺激を得るためのもの」とは認識していないようです。 これに対しては読書会のように複数人で一つの本を読んで、読み方の違いを共有するのが良いのではないかと予想しています。 読書会は同期的なイベントで、時間調整の都合があり、今の所試したことはありません*5

論理的思考力

「プログラミングが論理的思考力を育てる」という言説がありますが、あれは半分正解で半分不正解です。 論理的思考力は

  1. 論理を読み取る
  2. 論理を適用する
  3. 適用を検証する

のプロセスで成り立っています。 プログラミングが強いのは3の「適用を検証する」環境を作るのが早いところです。 ただ、早すぎるのです。1と2を飛ばしても作れてしまうのです。

つまり

ググり&コピペで向かい撃つ、素手で熊を殴り続ける

部分だけが繰り返せるのです。

「論理的思考力を育てる」面に関しては、検証環境の作りやすさが、既存の手工業よりは簡単という程度に思えます。

  1. 論理を読み取る
  2. 論理を適用する

が、できる人にとっては、検証環境の作りやすさは大きな武器になると思います。 これはインプットとアウトプットの能力があると同値です。 プログラミングは、自習できる人は大きく力を伸ばせるが、できない人はグラップラーになる分野のようです。

そして自習に必要な、日本語の読み書きの能力はOJTでは伸ばせません。 プロジェクト中の教育で「日本語の読み書きを教える」って、意味わからないですよね? ブートキャンプ的な教育でもそうです。日本語教えるよりプログラミング言語を教えたいですよね? 採用の時に日本語能力でフィルタリングするといいのかもしれません。

*1:現時点は受け入れ側企業としては、プログラミングスクールの卒業生は「即戦力」とは言い難いと感じています。自己投資に一定額以上を投入する気があって、プログラミングに興味がある人のフィルタリングには使えるとは思っています。

*2:一般論として「常駐の多いSIerでは、社内教育制度にある程度制約が掛かる」と判断して良いと思います。

*3:http://www.mext.go.jp/component/b_menu/shingi/giji/__icsFiles/afieldfile/2015/11/09/1363415_006.pdf

*4:ブログをこんだけ書いていて、技術同人誌を3冊書いて、技術同人誌のレビュー経験もある、日本語ライティング強者の感想です。すべてのプログラマが今すぐできることは保証しません。

*5:つまり完全に仮説です。検証していないので、問題設定があっているかすらわかりません。

2018年のふりかえりだとか2019年の目標だとか

2018年のふりかえり

発表

5回発表しました。

speakerdeck.com

Qiitaの記事のテキスト処理をして遊んでいたので、Qiitaのファンミーティングで紹介していみました。 PANQは今でも動いてはいますが、最近は開発を止めています。

speakerdeck.com

RubyKaigiでのLTです。 2018年は10年ぶりぐらいにRubyKaigiに参加しました。 久しぶりの参加なので、自己紹介代わりにLTをしました。 このLTは採用されやすくするため英語で発表しました。 発音で伝わらないと困るのでトークスクリプトをそのまま資料にしました。

speakerdeck.com

Node学園の勉強会で発表しました。 PANQの開発中にCPUヘビーな処理が必要になりました。 Node.jsのコアを使い切れない特徴とがっつり遊べる機会だったので、その話をしました。 発表直前にWebWorkerが公開されたのも良いタイミングでした。

特に結論がある発表ではなかったのですが、提示した疑問点で懇親会の議論が盛り上がって楽しかったです。

speakerdeck.com

Tokyo Rubyist Meetupにて発表。 内容はRubyKaigiのLTと同じです。本格的な発表にしました。 これも英語で発表しました。

WebSocketと比較して、SSEを推す人が居たのが新鮮でした。 WebSocketが実用できるタイミングでサーバPushに取り組んだため、僕はSSEの知見がありません。 細かい知見のやり取りができればよかったのですが、英語が・・・・ 発表はまだいいのですが、質疑応答に大苦戦しました。 質問の英語が聞き取れないのが辛いです。

speakerdeck.com

Rails Developers MeetupにてRailsについての発表です。 以前はフロントエンドをやっていて、Railsを本格的に使い始めて2年、発表できるまでの知見が貯めれてよかったです。

トラック数が多かったのでマニアックな話をそのままマニアックに話しました。 それまでの発表で、扱っている要件がレアなことがわかってきました。 20分中6分、要件の説明に多めの時間を取りました。

同人誌執筆

現代webフロントエンドデザインパターン

ledsun.booth.pm

技術書典4向けに書いた新刊です。 今年はサーバーサイドの仕事が増えることがわかっていたので、フロントエンドの知見を棚卸しする意味でデザインパターン形式にまとめました。 パターン形式だと先に目次を使って、中身を後で埋めることができます。 執筆が計画的に進められて良いです。

紙を買ってくれた人に悪いので、無料公開する予定はありません。

お仕事周り

開発

携わっている案件の開発箇所が移動したので、それに合わせてフロントエンドからバックエンドの開発がメインになりました。 また、会社がRailsに力を入れているのもあって、一番使う言語がJavaScriptからRubyに変わりました。

細かい技術的な話は、別に書くかもしれません。

マネジメント

片手手間でやっているマネジメント業の割合が増えてきました。

マネジメントの話をすると、技術の話をしているときより、同僚から辛辣なお言葉*1がいただけるので辛いです。 「面従腹背よりは良いはず」と自分に言い聞かせながら頑張っています。

面談

面談制度を整備しました。 面談のやり方が個人任せだったのを会社として方法を統一しました。

結論としては、面談から「ふりかえり」機能を取り除きました。 「ふりかえり」を実行するには、面談官にコーチング、ティーチングのスキルと、それを使い分けるスキルが必要です。 それらを面談官に要求するのは時期尚早と判断し*2、定期的なコミュニケーションを維持することを主目的にしました。

代わりに2019年は面接官を増やして、社内のコミュニケーションパスを増やす予定です。

RubyKaigi参加支援

参加支援の予算を獲得し、若者を二人連れていきました。 彼らの刺激にはなったようです。 一人にはRubyKaigiで興味を持った発表から、「RubyでつくるRuby」を紹介し、読んで学んだことを社内発表させました。

社内発表支援

2つの発表のお手伝いをしました。

1つ目は前述の「RubyでつくるRuby」の発表の支援です。 初回はすごく伝わらない発表でした。 案件での振る舞いも勘案した僕の分析上、日本語ライティング能力が足りていないようでした。 そこで読書レポート(新人エンジニアにレポートを書かせて技術書の読み方を伝える。 - @ledsun blogの形式)を書いてもらい、それを添削して日本語ライティング能力を伝えました。 添削回数なんと9回。我ながら頑張りました。

それで、自分の考えを整理してもらった上で、再度社内発表してもらいました。 二度目はちゃんと伝わる発表になりました!

2つ目は、社内の新人研修担当者が良い仕事をしていたので、その内容を社内向けに発表してもらいました。 新人研修のような地味な仕事は、社内の同僚から評価されにくにので、良い仕事をしていることを認識してもらうために、発表してもらいました。 研修の内容もかなり良いです。高度な内容を教えているという意味ではなく、受講者にフィットした研修をしているという意味で良いです。

研修内容は会社のHP(採用情報 | 株式会社ラグザイア)に、概要を書いてあります。 体制はめちゃくちゃ手厚いです。 日本で1 of the best 技術研修と言って差し支えないと思っています。

社内発表は好評だったので、2019年はエンジニア全員強制的に発表することにしました。 実は5年ぶり2回目の挑戦です(社内勉強会はヤメだ。自主的はいらん、全員技術発表だ! - @ledsun blog)。

ペアプロ

id:vestige に「面談で相手によって、情報交換が上手く行くときと行かないときがある」と相談をした時に「一緒に作業したらもっと色々わかるんじゃない?」とアドバイスをもらいました。 それで社内でペアプロに挑戦しました。

教育目的で、プロジェクトが違う若手とシニアでペアを組んでみました。

所管としては、プロジェクトが違う人に対して指導するほうが、プロジェクトの納期のプレッシャーを気にしなくて良いので、教育に集中できるように思います。 (メンバーの教育を(有期の)プロジェクトリーダーの責務に入れてはいけない - @ledsun blog

まだ、1〜2回しか実施できていないので、道半ばです。 2019年はもう少し回数を重ねて、ペアプロスキルを高めたいです。

技術同人誌のレビュー

何年か前にレビューしたときのお礼を今年になって言われる機会がありました。 調子に乗って、技術同人誌のレビューをやりました。

inutetraplus.booth.pm

フロントエンドをやっていたことはありますが、WebComponentsえをやったことはありませんでした。 レビューをするだけで、最新状況にキャッチアップできるというお得な体験でした。

興味が出てきたので、次フロントエンドやるときはWebComponentsも使いたいです。

周りの人を強化する

2018年に気づいた自分の謎能力です。 今の所上手く言語化できていません。

結果から推測すると、僕は自分の周りの人の能力を強化しているように思えます。 「社内発表の支援をする」のような、能動的にやっていることはそれ以外にもあるように思います。 要因は「肯定的なスタイルで人の話を聞く」のような気がしますが、よくわかりません。

案件であれば「要件の整理やヤックシェービングを巻き取って、若手に実装をガンガンやらせる」みたいなアプローチをすることはあります。 ボトルネック発見能力とキャッチアップ能力の合わせ技です。 この辺は自覚している得意分野です。

それとも違うような気がします。

2019年

春か秋かわかりませんが同人誌を一冊は書きたいです。

OSSへのコントリビューション数を増やしたいです。 お仕事でOSS開発しているので、元々コントリビューション数自体は多いのですが、お仕事と関係ないOSSへのコントリビューションを増やしたいです。 目的は

  • 日頃お世話になっているののお礼というのが一点
  • 技術力の研鑽が一点
  • 技術者としてのポゼッションを増やすというのが一点

です。

2018年の発表はニッチな領域に攻めることで価値を高めました。 2019年は、もっと多くの人の役に立つ発表がしたいです。 ニーズと合うかは時の運なので「できれば」ぐらいの目標です。

英語のトーク力を増やしたいです。 これは1年でなんとかなる気がしないので、長い目で鍛えて行こうと思います。

厄年らしいので健康に気をつけていきます。 運動習慣を身につけようと思います。

*1:人の出したアイデアに「無駄」とか「無意味」とか断定的なお言葉をガンガン頂けます。

*2:「ふりかえり」を目指すこと自体にめちゃくちゃ反対されました。それから必要なスキルをトレーニングするリソースも現段階ですぐには確保できなさそうです。

Ruby2.6 に関数合成オペレータが追加されたのでFizzBuzzで遊ぶ

fizzbuzz = -> x { x % 15 == 0 ? 'FizzBuzz' : x % 5 == 0 ? 'Buzz' : x % 3 == 0 ? 'Fizz' : x }
p_fizzbuzz = -> x { p x } << fizzbuzz

1.upto(15){ | n | p_fizzbuzz.call(n) }

pと合成するだけでは面白くないですね。

fizz = -> x { (x.is_a? Integer) && x % 3 == 0 ? 'Fizz' : x }
buzz = -> x { (x.is_a? Integer) && x % 5 == 0 ? 'Buzz' : x }
fizzbuzz = -> x { (x.is_a? Integer) && x % 15 == 0 ? 'FizzBuzz' : x }
do_fizzbuzz = fizz << buzz << fizzbuzz

1.upto(15){ | n | p do_fizzbuzz.call(n) }

条件に一致したら文字に変換、一致しなかったら素通しする関数にわけて合成してみました。

関数合成の一般的な良い使い道は特に思いつきません。

参考:プロと読み解く Ruby 2.6 NEWS ファイル - クックパッド開発者ブログ

#railsdm Rails Developers Meetup 2018 Day 4 Nouvelle Vague で発表しました

Rails Developers Meetup 2018 Day 4 Nouvelle Vague で発表してきました。 趣旨は「ActiveRecordやActiveJobなどのRailsの用意した抽象インタフェースを使うと、アプリケーションのミドルウェア構成、ひいてはアーキテクチャの決定を遅らせる事ができる。その分開発に集中できる」です。

speakerdeck.com

Rails中・上級者向けの話をして良いとのことだったので、Rails開発のレアのシチュエーションの話をしました。 そもそも対象のアプリケーション要件が特殊なので、その説明に20分中5〜6分使いました。

内容が少しマニアックすぎたので、伝わる相手は多くはなかったようです。 その中でも良いリアクションを頂けたので、発表した身としては大変満足でした。

反応

私の前にRail Wayはなく、私の後ろにRail Wayはある*1

補足

Webフロントエンド

Web用フロントエンドとブラウザの間はWebSocketでつないで、サーバから非同期に結果を送っています。 今回はWeb用フロントエンドに関わる話はしないので、端折りました。

Web用フロントエンドでの、WebSocket(とスレッド)に関わる、技術的落とし穴の話は下でしました。 speakerdeck.com アプリケーションの構成は説明していません。

Sucker Punch

qiita.com

発表時に詳細を端折った、Sucker Punchに当てるモンキーパッチの説明をQiitaに書きました。

同じく端折ったDockerの話もどっかーに書きたいです。

ActiveRecord

発表後に聞いた話では、該アプリケーションはSQLiteからPostgreSQLへ、ソースコード修正無しで、乗り換えできたそうです。 ActiveRecordの抽象化の完成度、すごいですね。

その他

動画もあるみたいです。恥ずかしいので自分では、見ていません。

twitter.com

*1:実際は先人が作った巨大なRail Wayの端っこを、ちょっと開拓しただけです。

builderscon tokyo 2018 で得たもの #builderscon

一ヶ月前の話ですが、やはり印象深かったので書きます。

植山類さんのセッション

大変良かったです。 僕は類マニアなので、以上に高いテンションになっている可能性があります。 その辺は差っ引いて読んでください。 新しい知見が得られたというか、目の前で人間が喋っているのが聞けたのが良かったです。

主な内容は以前のブログ記事と大体同じです。 note.mu

動画も公開されているようです。 builderscon.io

ハートが撃ち抜かれた

特に印象的だったのは「カーゴ・カルト・プログラミングを徹底的に避ける」話です。 勝手な想像ですが、カーゴ・カルト・プログラミングを避けた結果、ソースコードを理解していない部分がなくなり、アドバイスをくれる誰よりも自分がソースコードを理解している自信になり、定石破りが(自分のシチュエーションでは定石が有効でないことを喝破)できたのではないかと思います。

それでlldの性能と開発効率が上がって、多くのOSSプロダクトに採用されるようになった実績を、目の前の人間が喋っていることがすごいです。 ハートが撃ち抜かれた。

Turing Complete FMで何度も聞いている声ですが、実際に喋っているのをみると、最初から天才プログラマだったわけじゃなく、本人の努力で今があるのがわかって良いです。ブログ記事には上手く行った話を書くことが多いので、ブログだけ読んでいると「超天才か!」と思うのですが、「人間、天才みたいな属性だけで雑に説明できるわけねえよ」が実感できて良いです。

カーゴ・カルト・プログラミングを避ける

それで、感銘を受けて、「遅い処理を非同期化する」タスクに取り組んでいる時に、

  1. 「遅いのはわかるけど、どこが遅いか計測すべき」と計測し、データ作成でなく、データ送信に時間が時間がかかっていることを確認
  2. データ送信の振る舞いをみるとデータの送信量に依存せずに遅くなることがあるので、厳密にはデータ送信ではなく、コネクション確立に時間がかかっている
  3. ソースコードにコネクションを使い回す修正を入れて計測してみる
  4. 既存のソースコードには修正を邪魔する、不要な抽象が挟まっている。自分で書いたし、書いたときは良かれと思って書いています。まずは、この抽象をはずす

それから「コネクションを使い回す実装をいれ、それで再度計測して効果があるか確認する必要があるな」と、のたうち回っているところです。

当初は「非同期化するだけなら、スレッドかActiveJobを使えばいいだけ」と軽く見ていたタスクでした。それでも、「カーゴ・カルト・プログラミングを避ける」気持ちで見ると、既存の実装の良くない点(コネクションを使い捨てている)と、設計の良くない点(不要な抽象がある)が、見つかり新たな知見が得られます。

まだ調査中なので、本当にこの方針で実装するのが正しいのかはわかりません。例えば、送信先に依存しているかもしれない。そうならば送信先を制限した方がいいかもしれません。それでも、この調査で得られた知見はより正しい判断につながるでしょう。すくなくとも、不要な抽象(本当に何の役にもたってなさそう)が見つかったのは、この先ソースコードに技術的負債を抱えて進んでいくより良いでしょう。この辺は、時間的プレッシャーに負けそうな自分に言い聞かせている理由付けです。

そういうお気持ちでプログラミングを進めていきたい。

メンバーの教育を(有期の)プロジェクトリーダーの責務に入れてはいけない

t.co

を読んで考えた話です。

30歳近くになっても無能、ということは、そいつはほとんどの場合、一生無能だ

と刺激的な言葉が使われています。 状況を限定すれば、一理あると感じる点があります。 表題の「メンバーの教育を(有期の)プロジェクトリーダーの責務に入れてはいけない」です。

結論

プロジェクトリーダーの最大の責務はプロジェクトを成功です。 メンバーの教育は、プロジェクト成功の阻害要因になっているときだけやります。

無能なやつには、意見を聞いても、ろくな話は出てこない。時間がもったいない

の例では、「意見を聞かない」という簡単な回避策があります。 回避策があるときは、教育の手間を掛けてはいけません。

そうは言っても、一緒に仕事する仲間はできるだけ賢くあって欲しい、自分の足を引っ張らないで欲しいと思うのが人間です。

教育は誰がやればいいのか?

ティーチングとコーチン

メンバーの教育は上司(ライン上の上司)がやります。 ここで、教育を大きく2つにわけます。 ティーチングとコーチングです。

プロジェクトに直結したティーチングはプロジェクトリーダーの責務です。 例えば「gitの操作に不慣れなメンバーに、gitの使い方を教える」(または教える人を割り当てる)のはプロジェクトリーダーの仕事です。

コーチングでは、本人に思考を促し、失敗に対して工夫し、何度か失敗を繰り返すのを見守り、成功まで導きます。 要はPDCAサイクルを回すサポートをします。 これは上司の責務です。

コーチの責務

コーチングの目的は、未知の問題にぶつかった時に、自ら工夫してチャレンジしていく能力と自信を身に着けさせることです。 本人が試行錯誤して成功にたどり着く必要があります。 時間が必要です。 またコーチ役は「教えちゃえば簡単なのに」を我慢する必要があります。

一人の人間が同時に、プロジェクトの成功を急ぐのと、メンバーの成長を待つのをやるのは無理です。 人を分けましょう。 プロジェクトリーダーが安心して、教育を手放せるように、他に教育を責務とする人を用意しましょう*1

*1:コーチングを上司の責務にして、プロジェクトリーダーの責務から引き剥がすのが、組織を作る人(CTO、VP of Engineering、人事部門)の責務です。

革ベルトを作る計画を立案せよ

動機

レザーパンチを買ってベルトに穴を開けていたら、革ベルトが作りたくなりました。

予算と手順を立ててみます。

方針

なるべく工数を下げ、可能な限り早く完成にたどり着いて、お手軽に達成感を得ます。

そのため、

  • 手作業での革のカット
  • 手作業での革の縫い合わせ
  • スタッズ等の装飾
  • バックルなどパーツへのこだわり

は放棄します。加工済みの部品で値段が安いものを組み合わせて作成します。

予算

種類 名前 URL サイズ 単価 数量 金額 送料
ベルト 昭南多脂ベンズ ベルト ナチュラ http://l-phoenix.shop-pro.jp/?pid=62568295 4cm 2600 1 2600
バックル固定部分漉き加工 http://l-phoenix.shop-pro.jp/?pid=46123850 200 1 200
バックル バックル 415-35 真鍮 http://l-phoenix.shop-pro.jp/?pid=88940283 680 1 680
ループ ループ35-1B http://l-phoenix.shop-pro.jp/?pid=97645764 340 1 340
カタビス http://l-phoenix.shop-pro.jp/?pid=62568295 9x4x5 70 4 280
トコノール https://item.rakuten.co.jp/lc-palette/s72002/ 324 1 324 220

総額 4,644円

作業手順

  1. バックルを通す用の穴位置を決める
  2. バックルを通す用の穴を開ける
  3. バックルを取り付ける穴の位置を決める
  4. バックルを取り付ける穴を開ける
  5. バックルを取り付ける
  6. 穴の位置を決める
  7. 穴を開ける
  8. 理想の長さを測る
  9. ベルトを切る
  10. トコノールで断面を処理する
  11. ミンクオイルで表面を磨く

11手順、1手順15分として3時間程度で完成できそうです。

参考URL

ミントチョコレートの起源

アイスに限らずにミントとチョコレートの組み合わせの起源を探ります。

1962年 アフターエイト

アフターエイトは初めて知った大人のミントチョコレート : イギリスの食、イギリスの料理&菓子

アフターエイトは、1962年にイギリスで生まれたチョコレート菓子。

単純にチョコレートとミントを組み合わせるだけなので、一番古い組み合わせなのかと予想していました。 1962年で予想外に新しいです。

1945年 チョコミントアイス

チョコミントの発祥はどこ?ミントがトルコならチョコミントは?

1945年にアメリカのカルフォルニアでバスキンさんとロビンスさんによって バスキン・ロビンスという世界最大のアイスクリームのチェーン店が誕生しました。

これがサーティーワンの発祥です。

現地では、この頃からチョコミントフレーバーは人気のアイスでした。

チョコミントと言えばアイスぐらいの王道だけあって、 1945年には既にチョコミントアイスがあるようです。

1900年 グラスホッパー

カクテルガイド|カクテルを知る

現代式のカクテル=「器具を使って作る氷で冷やしたミクスト・ドリンク」となったのは1879年の製氷機の発明以来です。

アイスクリームが大衆化するよりは、カクテルが普及した方が古いので、チョコミントアイスより古そうです。

グラスホッパー・カクテル(Grasshopper Cocktail)のレシピ・作り方| しっぽり...

サンフランシスコにある有名な老舗ホテルのパレス・ホテル(The Palace Hotel)のバーテンダーだったハリー・オブライエン(Harry O'Brien)が1900年頃に考案した

1900年。予想通り、今の所発見できた最古の情報です。

だとすると、ミントチョコレートはもともとカクテルの味として誕生した、大人の味だったのでしょうか? お菓子のチョコレートとミントを組み合わせる方が作るのが簡単そうです。

チョコレートの歴史 - Wikipedia

1875年にミルクチョコレートの販売を始めた。またミルクチョコレート製造には、牛乳から水分を抜く必要があったが、ダニエルは隣りに住んでいたベビーフード生産業者のアンリ・ネスレネスレ創業者)と協力して研究を行った

甘いチョコレートが普及したのが19世紀末なようです。 ミントとチョコレートの組み合わせは、グラスホッパーが起源で、アメリカ生まれの、可能性もありそうです。

Ruby on Railsの開発環境でマルチスレッドでクラス定義を探索すると刺さるが再現できなかった話

現象

Ruby on Railsの、ActiveJob内で起動したスレッドで非同期にDBに書き込もうとすると、ActiveRercordのクラス探索で無限に待って固まります。

わかっている条件

  • ActiveJobで起きる
  • ActiveJobのQueueAdapterにはAsync adapterを使っている
  • DBへの接続以前のActiveRercordのクラス探索で固まる

例えば

Thread.start do
  executor = Lodqa::OneByOneExecutor.new dataset, query, debug: false

  # Bind events to colletc answers
  collected_answers = []
  executor.on(:answer) do |_, val|
    Answer
  end
end

このような処理*1をApplicationJobクラスで実行すると、Answerの探索で固まり、無限に待ちます。

Thread.startでスレッドを開始するほか、executor内でEventMachine#deferを使って更に複数スレッドで処理を行っています。

再現の努力

そこで、再現する最小のソースコードを書いてみます。

class CircularDependencyDetectedJob < ApplicationJob
  queue_as :default

  rescue_from(StandardError) do |exception|
    logger.fatal exception
  end

  def perform(*_args)
    Thread.new { EventMachine.run }

    1.times do
      Thread.new do
        2.times do
          EM.defer do
            Answer
            p 'Goal!!!'
          end
        end
      end
    end
  end
end

エラーが発生

これを実行すると、固まりはしませんが、次のようなエラーが発生します。

/usr/src/myapp # rails c
Loading development environment (Rails 5.2.0)
irb(main):001:0> CircularDependencyDetectedJob.perform_later
Enqueued CircularDependencyDetectedJob (Job ID: 9676f7ff-5c23-411c-84e3-33aca66c5eae) to Async(default)
=> #<CircularDependencyDetectedJob:0x0000562444702420 @arguments=[], @job_id="9676f7ff-5c23-411c-84e3-33aca66c5eae", @queue_name="default", @priority=nil, @executions=0, @provider_job_id="39bf3d7d-051f-4915-a752-17686211c958">
irb(main):002:0> Performing CircularDependencyDetectedJob (Job ID: 9676f7ff-5c23-411c-84e3-33aca66c5eae) from Async(default)
Performed CircularDependencyDetectedJob (Job ID: 9676f7ff-5c23-411c-84e3-33aca66c5eae) from Async(default) in 62.71ms
#<Thread:0x000056244503a970@/usr/local/bundle/gems/eventmachine-1.2.7/lib/eventmachine.rb:1067 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
    7: from /usr/local/bundle/gems/eventmachine-1.2.7/lib/eventmachine.rb:1077:in `block in spawn_threadpool'
    6: from /usr/src/myapp/app/jobs/circular_dependency_detected_job.rb:16:in `block (4 levels) in perform'
    5: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
    4: from /usr/local/bundle/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
    3: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:534:in `load_missing_constant'
    2: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
    1: from /usr/local/bundle/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
/usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:500:in `load_missing_constant': Circular dependency detected while autoloading constant Answer (RuntimeError)
#<Thread:0x00005624446907f8@/usr/src/myapp/app/jobs/circular_dependency_detected_job.rb:9 aborting> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
    7: from /usr/local/bundle/gems/eventmachine-1.2.7/lib/eventmachine.rb:1077:in `block in spawn_threadpool'
    6: from /usr/src/myapp/app/jobs/circular_dependency_detected_job.rb:16:in `block (4 levels) in perform'
    5: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
    4: from /usr/local/bundle/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
    3: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:534:in `load_missing_constant'
    2: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
    1: from /usr/local/bundle/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
/usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:500:in `load_missing_constant': Circular dependency detected while autoloading constant Answer (RuntimeError)
Traceback (most recent call last):
    7: from /usr/local/bundle/gems/eventmachine-1.2.7/lib/eventmachine.rb:1077:in `block in spawn_threadpool'
    6: from /usr/src/myapp/app/jobs/circular_dependency_detected_job.rb:16:in `block (4 levels) in perform'
    5: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
    4: from /usr/local/bundle/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
    3: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:534:in `load_missing_constant'
    2: from /usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
    1: from /usr/local/bundle/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
/usr/local/bundle/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:500:in `load_missing_constant': Circular dependency detected while autoloading constant Answer (RuntimeError)

エラーの原因

該当のソースコードを読むと

if loading.include?(expanded)
  raise "Circular dependency detected while autoloading constant #{qualified_name}"
else

loadingに自身が含まれていたら、循環依存で読み込みをやめる処理です。 マルチスレッドで読み込むと、循環依存していなくても、このチェックをするタイミングで他スレッドで自クラスを読み込んでいることがあるので、発生しているようです*2

対応

この処理は定数の自動読み込みと再読み込み | Rails ガイドのための処理です。 自動読み込みを停止してみましょう。

Rails アプリケーションを設定する - Railsガイド

config.eager_loadをtrueにすると、config.eager_load_namespacesに登録された事前一括読み込み(eager loading)用の名前空間をすべて読み込みます。ここにはアプリケーション、エンジン、Railsフレームワークを含むあらゆる登録済み名前空間が含まれます。

config/environments/development.rb

config.eager_load = true

を指定すると、このエラーは起きなくなります。

やはり、「定数の自動読み込み」と関係がありそうです。

残る謎

  • マルチスレッドであれば起きるはずなのに、Rails consoleで次のソースコードを実行しても発生しない
1000.times { Thread.new { Answer } }
  • 元の現象では固まるのに、再現コードではエラーが発生する

暫定の結論

元のソースコードの、クラス探索で固まる現象もeager_loadを有効にすると発生しなくなります。 これ以上調査時間が取れないので、この記事で供養して完了とします。

*1:発生したソースコードと完全には一致していません。

*2:GILがあるから並列に動かないし衝突しないはず・・・と思ったら、ファイルからクラスを読み込む処理なので並列に動くようです。