@ledsun blog

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

天才王子の赤字国家再生術 ~そうだ、売国しよう~

9巻まで読みました。 ライトノベルファンタジー。魔法は出てきません。ただ主人公が天才という理由で政治、外交、戦争で無双するお話。「敵に裏を読まれたら、主人公は裏の裏まで読んでいた!」な爽快感が味わえます。安心して読めます。

ジャンルは違いますがACMA:GAMEと似ている感じがしました。

アニメ化するみたいです。 tensaiouji-anime.com

Ruby 2.5とRails3.2のときrake db:createでString can't be coerced into Integerが発生する

現象

~ bundle exec rake db:create
Deprecation warning: Expected string default value for '--quiet'; got false (boolean).
This will be rejected in the future unless you explicitly pass the options `check_default_type: false` or call `allow_incompatible_default_type!` in your code
You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
Deprecation warning: Expected string default value for '--syslog'; got false (boolean).
This will be rejected in the future unless you explicitly pass the options `check_default_type: false` or call `allow_incompatible_default_type!` in your code
You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
Deprecation warning: Expected string default value for '--logfile'; got true (boolean).
This will be rejected in the future unless you explicitly pass the options `check_default_type: false` or call `allow_incompatible_default_type!` in your code
You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
String can't be coerced into Integer
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-3.2.22.2/lib/active_support/core_ext/enumerable.rb:60:in `+'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-3.2.22.2/lib/active_support/core_ext/enumerable.rb:60:in `sum'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-3.2.22.2/lib/active_support/core_ext/enumerable.rb:60:in `sum'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/connection_adapters/postgresql_adapter.rb:747:in `create_database'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:144:in `rescue in create_database'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:84:in `create_database'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:62:in `block (3 levels) in <top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:62:in `each'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:62:in `block (2 levels) in <top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:240:in `block in execute'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:235:in `each'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:235:in `execute'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:179:in `block in invoke_with_call_chain'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/2.5.0/monitor.rb:235:in `mon_synchronize'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:172:in `invoke_with_call_chain'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:165:in `invoke'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:150:in `invoke_task'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:106:in `block (2 levels) in top_level'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:106:in `each'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:106:in `block in top_level'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:115:in `run_with_threads'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:100:in `top_level'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:78:in `block in run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:176:in `standard_exception_handling'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:75:in `run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/bin/rake:33:in `<top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/rake:23:in `load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/rake:23:in `<top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli/exec.rb:74:in `load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli/exec.rb:74:in `kernel_load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli/exec.rb:28:in `run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli.rb:463:in `exec'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli.rb:27:in `dispatch'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli.rb:18:in `start'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/exe/bundle:30:in `block in <top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/exe/bundle:22:in `<top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/bundle:23:in `load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/bundle:23:in `<main>'
Couldn't create database for {"adapter"=>"postgresql", "encoding"=>"unicode", "database"=>"pubannotation", "pool"=>5, "host"=>"localhost", "username"=>"postgres", "password"=>"password"}
String can't be coerced into Integer
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-3.2.22.2/lib/active_support/core_ext/enumerable.rb:60:in `+'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-3.2.22.2/lib/active_support/core_ext/enumerable.rb:60:in `sum'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-3.2.22.2/lib/active_support/core_ext/enumerable.rb:60:in `sum'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/connection_adapters/postgresql_adapter.rb:747:in `create_database'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:144:in `rescue in create_database'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:84:in `create_database'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:62:in `block (3 levels) in <top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:62:in `each'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activerecord-3.2.22.2/lib/active_record/railties/databases.rake:62:in `block (2 levels) in <top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:240:in `block in execute'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:235:in `each'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:235:in `execute'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:179:in `block in invoke_with_call_chain'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/2.5.0/monitor.rb:235:in `mon_synchronize'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:172:in `invoke_with_call_chain'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/task.rb:165:in `invoke'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:150:in `invoke_task'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:106:in `block (2 levels) in top_level'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:106:in `each'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:106:in `block in top_level'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:115:in `run_with_threads'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:100:in `top_level'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:78:in `block in run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:176:in `standard_exception_handling'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/lib/rake/application.rb:75:in `run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/rake-10.5.0/bin/rake:33:in `<top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/rake:23:in `load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/rake:23:in `<top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli/exec.rb:74:in `load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli/exec.rb:74:in `kernel_load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli/exec.rb:28:in `run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli.rb:463:in `exec'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli.rb:27:in `dispatch'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/cli.rb:18:in `start'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/exe/bundle:30:in `block in <top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
/Users/shigerunakajima/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-1.17.3/exe/bundle:22:in `<top (required)>'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/bundle:23:in `load'
/Users/shigerunakajima/.rbenv/versions/2.5.7/bin/bundle:23:in `<main>'
Couldn't create database for {"adapter"=>"postgresql", "encoding"=>"unicode", "database"=>"pubannotation_test", "pool"=>5, "host"=>"localhost", "username"=>"postgres", "password"=>"password"}

原因

エラーが起きているのは https://github.com/rails/rails/blob/3-2-stable/activesupport/lib/active_support/core_ext/enumerable.rb#L58-L64 です。

 def sum(identity = 0, &block)
    if block_given?
      map(&block).sum(identity)
    else
      inject(:+) || identity
    end
  end

Ruby 2.4で Enumerable#sum が実装されたことが原因のようです。 60行目で呼ばれるsumActiveSupportのEnumerable#sumからRubyのEnumerable#sum に変わるようです。 なぜそうなるのかはよくわかりません。

Ruby 2.5でのみこのエラーが起きることを確認しました。 おそらくRuby2.4でも起きると思います。

対策

rake db:create以外は正常に動くので、psqlPostgreSQLにログインして、次のSQLを実行してdatabaseを作成しました。

CREATE DATABASE "pubannotation" ENCODING = 'unicode';
CREATE DATABASE "pubannotation_test" ENCODING = 'unicode';

参考

OSSライブラリからの学びかた npmからJavaScriptのライブラリを探してソースコードを見つける編

はじまり

blog.magnolia.tech

CPANに上がってるモジュール、一つ一つの粒度が小さいから読みやすいし、ドキュメントもテストもしっかり揃ってて挙動を把握しやすくて、自分にとっては最高の教科書だった

OSSで公開されているソースコードは、最高の教科書ですよね。 Perlにはなじみがないので、卑近な例としてJavaScriptの場合を考えてみます。

npmからJavaScriptのライブラリを探してソースコードを読む

例えばHTMLエスケープの実装が知りたいとき

HTML文字列を生成する際に、ユーザー入力をそのまま出力するとタグが生成されてHTMLが壊れることがあります。 というかXSS脆弱性です。そこでHTMLエスケープをしたいなと思うのですが・・・どう実装したら漏れなく対応できるかわからない状況を仮定しましょう。

npmリポジトリを検索

JavaScriptではnpm | build amazing thingsというパッケージリポジトリが主流です。 まずはここで検索します。

HTMLエスケープしたいので、雑にhtmlescapeで検索してみましょう。 https://www.npmjs.com/search?q=html%20escape

f:id:ledsun:20210108134104p:plain
npmjs.orgでhtmlとescapeで検索した結果

224パッケージも出てきて、どれを見たらいいか迷います。 まずは一番うえのhtml-escaperから見てみましょう。

https://www.npmjs.com/package/html-escaper

f:id:ledsun:20210108134346p:plain
html-escaperのダウンロード数

まずダウンロード数を確認します。 700万とか異次元の数値が出ています。 広く使われているようです。

参考までに2番目のxssパッケージも見てみましょう。

f:id:ledsun:20210108134559p:plain
xssのダウンロード数

90万件と十分に使われています。 が、html-escaperは一桁多いので、HTMLエスケープ用のパッケージとしては、html-escaperがもっとも広く使われていると推測できます。

つぎにhtml-escaperの実装を見てみましょう。 右側のHomepageの欄にGitHubへのリンクがあります。

f:id:ledsun:20210108134921p:plain
html-escaperのGitHubへのリンク

GitHubで実装を探す

https://github.com/WebReflection/html-escaper

f:id:ledsun:20210108135901p:plain
html-escaperのGitHubリポジトリ

JavaScriptのライブラリはindex.jsがエントリポイントになっていることが多いので、inedx.jsを見てみましょう。 一番下までスクロールすると、次のようにexportsescapeunescapeを代入している箇所が見つかります。

f:id:ledsun:20210108140143p:plain
html-escaperのexport

JavaScriptのCommonJSというパッケージでは、exportsオブジェクトを使って、定義した関数を公開します。 このescapeの実装が見つかれば、HTMLエスケープの実装がわかりそうです。

少し上にスクロールすると、次のように41行目でescapeが定義されている場所が見つかりました。

f:id:ledsun:20210108141012p:plain
escapeは41行目で定義されている

escape関数の実装を読む

次の定義からescapeはreplace関数を呼んでいることがわかります。

const escape = es => replace.call(es, ca, pe)

このreplaceは何でしょうか?

24行目でconst {replace} = '';と定義されています。 これはつまりString.prototype.replaceです。 分割代入を使って、組み込み関数への参照を取得する方法を初めて見ました。驚きです。勉強になりますね。

replacecallで呼び出しているので、es.replace(ca, pe)と同じです。 es => replace.call(es, ca, pe)とアロー関数で定義されているので、次の関数定義と同じです。

function espace(es) {
  return es.replace(ca, pe)
}

第一引数caは何でしょうか?28行目で定義されていいます。

const ca = /[&<>'"]/g;

HTMLエスケープで、置き換えたい文字を表す正規表現です。

第二引数peは何でしょうか?30〜37行目を見てみましょう。

const esca = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  "'": '&#39;',
  '"': '&quot;'
};
const pe = m => esca[m];

置き換えたい文字列をキーにして、置き換え後の文字列を返す関数です。 String.prototype.replaceは第二引数に関数を指定できます。 MDNでは次のように説明されています。

新しい部分文字列を生成するために実行される関数で、regexp や substr でマッチしたものを置き換えるのに使われます。この関数に渡される引数は下記の「引数としての関数の指定」で述べられています。

つまり、escape関数は引数で与えられた文字列で、escaマップに定義された文字の組み合わせで置き換えることがわかりました。 &, <,>, ", 'を置換できれば、十分有効にHTMLエスケープとして動きそうだと推測できます。

やりました。 ここまでで、HTMLエスケープの仕様を調べずに、HTMLエスケープの仕様をなんとなく把握しました。

オチ

いい感じにOSSのライブラリから実装を見つけられないときは、 大体「HTMLエスケープの実装が知りたい」みたいないい感じのゴールを設定できていない時なんですよね・・・。

2021年の目標設定

2021年の目標

テーマはプログラミング速度が速い分野を増やすです。

2020年は、特定のプロジェクトでのプログラミング速度の向上に成功しました。

とはいえ、今年のオープンソース活動振り返り @ 2020 | Web Scratch と見比べると大分遅く感じます。 また、2020の成功例は、プロジェクトや技術ドメインへの習熟度、プロジェクトへの関わり方に依存しているように思います。 そこで、他のプロジェクトでのプログラミング速度向上を目標とします。

サブ目標

  • publicなサイドプロジェクトのプログラムを完成させる
  • Ruby並行並列大全」のractor/fiber scheduler対応版を書く
  • TOEICの最高得点を更新する(英語の学習習慣を組み立てる)

2020年の取り組み

うまくいったこと

2020年のテーマは「使う道具を増やす以外の方法で技術力を上げる」でした。

2019年のふりかえりと2020の目標 - @ledsun blogによると、当初は次のような作戦を立てていました。

理解しているレイヤーを増やすようなアプローチが必要かな?と思っています。 「作って理解するOS」を読み始めました。まだ途中です。 2020年には、実装するところまでやりきりたいです。 2017年に「RubyでつくるRuby 」を読んだときは、考え方を応用してできることがぐっと増えたので、同じような効果を期待しています。

半年ほどたって、「論理的思考の放棄」をパクる - @ledsun blog

1日1000行のリファクタリングなら、できる気がする。

と感じて、作戦変更しました。 プログラミングそのものの速度を追求するアプローチです。

ふたたび Gitのdiffを振り返る - @ledsun blogによれば、どうやら成功したようです。 概ね次のような策を実施しました

  1. 論理的思考を放棄する
  2. コミット粒度を小さくする
  3. テスト実施頻度を増やす(テストを実施したらタグを打つ)
  4. テスト手順書をソースリポジトリにマージする

それぞれ次のような効果がありました。

  1. 机上で考える時間が減りコーディング作業の時間が増える。実施前に予想していたほどは、手戻りは起きませんでした。
  2. 手戻りが起きた際にコミット単位で戻せる。修正で予想外の影響が出たとき原因をコミット単位で特定できます。追加の修正が必要であればamendすればいいです。不要であればrevertすればいいです。
  3. 不具合が早くたくさんみつかるようになった。手動テストでは、テスト時に実施者の行動のブレから新しいバグが見つかることがあります。この現象はテスト実施時間を減らして、テスト実施頻度を上げた方が、よく起こるようです。また、一回のテスト実施で少ないテスト項目に集中できるので、テスト手順の修正が丁寧になります。テスト実施時間が短くなるとデバッグで、テスト実施時間を食い潰したと時に減る時間が短くなります。
  4. ソースコードとテスト手順を別リポジトリで管理していると、ワークスペースの切り替えコストが馬鹿になりません。これはテスト実施頻度が増えると影響が大きくなります。

アプローチを変更してから、明らかな効果が出るまで半年掛かりました。 半年程度頑張るとプログラミング速度は目に見えて上げられるようです。

あんまりうまく行かなかったこと

必要があって、2週間ほどSVGの勉強をしました。 2週間の間は、結構な時間をとって集中しましたが、成果は芳しくありませんでした。

SVGの概念の把握や周辺ツールの理解は進みました。 しかし、SVGで絵が描けません。 またSVGを操作したり、生成したりするプログラミングを書いたとして、それが仕事になるイメージができません。 要するに、新しい技術ドメインに取り組む場合、サイドプロジェクトで取り組んでも全然時間が足りないことがわかりました。

冷静に考えれば、2週間で仕事になるレベルで技術が身につかないのは、当たり前です。 ですが、知らない技術ドメインは解像度が低すぎて、不当に難易度を高くまたは低く見積もってしまいます。 メインプロジェクトとして取り組む時間が確保できれば、もう少し解像度が高くなるのでしょう。 ただ、その時点ではメインプロジェクトになる予定が延期されました。

メインプロジェクトで、技術力を上げられるアプローチを採用しました。

その他

またまた Gitのdiffを振り返る

ふたたび Gitのdiffを振り返る - @ledsun blog で日単位で集計しました。

週、月、年単位の集計結果も見ていきましょう。

gnuplot> set xdata time
gnuplot> set timefmt "%Y-%m-%d_%H:%M:%S"
gnuplot> plot 'all_log_week.dat' using 1:2 w i title 'insertions' lw 2, 'all_log_week.dat' using 1:3 w i title 'deletions' lw 2

f:id:ledsun:20201230223748p:plain
週毎のファイルの変更行数

グラフが見やすくてよいです。

gnuplot> plot 'all_log_month.dat' using 1:2 w i title 'insertions' lw 2, 'all_log_month.dat' using 1:3 w p title 'deletions'

f:id:ledsun:20201230223936p:plain
月毎のファイルの変更行数

これも全体的な傾向をつかめているように思います。

今後の追跡調査は、週または月で集計するのが良さそうです。

gnuplot> plot 'all_log_year.dat' using 1:2 w l title 'insertions', 'all_log_year.dat' using 1:3 w l title 'deletions'

f:id:ledsun:20201230224127p:plain
年毎のファイルの変更行数

2019年が5月開始なので、8ヶ月分のデータしかありません。 期間が1.5倍なので、増加量としては妥当な感じです。

insertions/deletionsの比率が1.1から1.3に増えているのが興味深いです。 機能追加が多かったのでしょうか?

ふたたび Gitのdiffを振り返る

続々 Gitのdiffを振り返る - @ledsun blogでコミット単位でのファイルの変更行数の遷移をふりかえりました。

コミット単位では列間が詰まりすぎてグラフが見にくいです。 特に、Gitのコミット数を振り返る - @ledsun blog で見たように、2020年9月以降コミット粒度を小さくしています。

f:id:ledsun:20201220211139p:plain
2020年の月別コミット数

このため列間がますます詰まり、グラフから情報が読み取りにくくなっています。 そこで、ファイルの変更行数を、再び、日、週、月、年の単位で集計します。

次のRubyスクリプトを使います。

require "Time"
require "Date"

duration = $*.shift.to_s.to_sym

Commit = Struct.new(:at, :insertions, :deletions)

# カウントしたくないファイルを除外
EXCLUDE = "':(exclude)package-lock.json' ':(exclude)*.min.js' ':(exclude)*.css' ':(exclude)dist/*' ':(exclude)dev/vender' ':(exclude)src/lib/modules' ':(exclude)*.md'"

`git log --after='2019-05-22 00:00' --format=format:'---%n%ai' --shortstat #{EXCLUDE}`
  .split("---\n")[1..] # 1行目は空なので捨てる
  .map { _1.split("\n") }
  .filter { |time, stats| !stats.empty? } # 除外しているファイルがあるのでstatsが出ないことがある
  .map { |time, stats| Commit.new(Time.parse(time), stats.split(",")[1].to_i, stats.split(",")[2].to_i) } # " 1 file changed, 10 insertions(+), 9 deletions(-)" をパース
  .filter { _1.insertions < 1000 } # 1000行以上の変更はツールによるので除外する
  .sort_by { _1.at.to_i }
  .reduce({}) do |result, item|
  case duration
  when :day
    date = Date.parse item.at.to_s # 日
  when :week
    date = Date.parse(item.at.to_s) - item.at.wday # 週
  when :month
    date = Date.parse item.at.strftime("%Y-%m-01") # 月
  when :year
    date = Date.parse item.at.strftime("%Y-01-01") # 年 は期間が違い過ぎるので、比較には向かない
  else
    date = item.at
  end
  if result.key? date
    result[date].insertions += item.insertions
    result[date].deletions += item.deletions
  else
    result[date] = Commit.new(date, item.insertions, item.deletions)
  end

  result
end
  .each_value do
  puts "#{_1.at.strftime("%F_%T")} #{_1.insertions} #{_1.deletions}"
end

引数で、dayweekmonthyearを受け取り、それぞれの単位で集計します。 出力データ形式続々 Gitのdiffを振り返る - @ledsun blogと一緒です。

gnuplot> set xdata time
gnuplot> set timefmt "%Y-%m-%d_%H:%M:%S"
gnuplot> plot 'all_log_day.dat' using 1:2 w i title 'insertions' lw 2, 'all_log_day.dat' using 1:3 w i title 'deletions' lw 2

f:id:ledsun:20201230165055p:plain
日毎のファイルの変更行数

続 Gitのdiffを振り返る - @ledsun blogのグラフと比べると大分ちがいます。 変更行数の変動が少なく、現実の作業効率を反映していそうです。

f:id:ledsun:20201227231409p:plain
異常値を除いた一日毎のファイルの変更行数

どうやら、本当に一日に2000〜2500行の変更をしているようです。

今年は 「論理的思考の放棄」をパクる - @ledsun blog に書いたように 1日1000行のリファクタリング を目指してプログラミングスタイルを工夫していました。半年を経て、どうやら達成したようです。 現時点では、一回しか達成していないので、再現性がないかもしれません。 来年は、これがフロックでないことを確認したいと思います。

「1日1万行とか、絶対無理じゃん」という思いもありましたが、あと4倍なら、俄然現実味が帯びて参りました。

続々 Gitのdiffを振り返る

続 Gitのdiffを振り返る - @ledsun blog で1000行以上の変更があるコミットはツールによるフォーマット変更であることがわかりました。

また、日付でまとめてしまうと一日の変更行数が2000を超えます。 これはgit rev-listのbeforeやafterの挙動によるものです。 必ずしもコミットした日にまとまっていません。

例えば、つぎとコミットがあったとします。

  • 1/10に機能追加のコミットA
  • 1/15にバグ修正のコミットB

バグ修正を先にpushしたかったので、rebaseして、コミットBをコミットAの前に移動します。 するとコミットAは1/15日以降のコミットにカウントされます。 このように実際の作業日とはずれて集計されます。 特にリリース直前の日に作業が固まりやすい傾向があります。

そこで期間中の全コミットをもってきて、そのままグラフ化します。

次のRubyスクリプトを使います。

require "Time"

Commit = Struct.new(:at, :insertions, :deletions)

# カウントしたくないファイルを除外
EXCLUDE = "':(exclude)package-lock.json' ':(exclude)*.min.js' ':(exclude)*.css' ':(exclude)dist/*' ':(exclude)dev/vender' ':(exclude)src/lib/modules' ':(exclude)*.md'"

`git log --after='2019-05-22 00:00' --format=format:'---%n%ai' --shortstat #{EXCLUDE}`
  .split("---\n")[1..] # 1行目は空なので捨てる
  .map { _1.split("\n") }
  .filter { |time, stats| !stats.empty? } # 除外しているファイルがあるのでstatsが出ないことがある
  .map { |time, stats| Commit.new(Time.parse(time), stats.split(",")[1].to_i, stats.split(",")[2].to_i) } # " 1 file changed, 10 insertions(+), 9 deletions(-)" をパース
  .filter { _1.insertions < 1000 } # 1000行以上の変更はツールによるので除外する
  .sort_by { _1.at.to_i }
  .each { puts "#{_1.at.strftime("%F_%T")} #{_1.insertions} #{_1.deletions}" }

次のような行が3000行続きます。

2020-12-25_17:22:45 4 4
2020-12-25_17:24:41 10 9
2020-12-25_17:27:11 9 3

結果をファイルにいれてgnuplotで表示します。

gnuplot> set xdata time
gnuplot> set timefmt "%Y-%m-%d_%H:%M:%S"
gnuplot> plot 'all_log.dat' using 1:2 with line title 'insertions', 'all_log.dat' using 1:3 with line title 'deletions'

f:id:ledsun:20201229222555p:plain
Gitコミット毎 の ファイルへの追加行数と削除行数

2019年と2020年で、プログラミングのピーク性能は特段変わっていないことがわかりました。

2020年に作ったGithubリポジトリ

2020年に作ったGitHubリポジトリ - pockestrapにインスパイアされました。

次の条件でGithubを検索します。

Search · is:public user:ledsun created:2020 · GitHub

f:id:ledsun:20201229123657p:plain
2020年にGithubに作ったリポジトリ

大体どれも作りかけです。

GitHub - ledsun/svg-playground: SVGの学習用の実験場です。

SVGおじさんに憧れて素振りしていた時のものです。素材をSVGにする部分が難しいなあ、素振りしてもなかなか身につかないなあ・・・というところで止まりました。

GitHub - ledsun/github-actions-continuous-delivery

Github Actionsの練習用

GitHub - ledsun/echos: Echosは、短い音声にURLを与える音声ストレージ

ブラウザで音声を保存します。構想では永続化しようと思っていました。getUserMediaで取得したデータをWaveフォーマットにエンコードするところまで書いて止まりました。

GitHub - ledsun/three-js-playground: Three.jsをスブって遊ぶ場所

WebGLの勉強がてらthree.jsを素振りしていた時のものです。three.jsがいい感じに抽象化してくれているおかげで、WebGLの概念が身につかないなあ・・・というところで止まりました。

GitHub - ledsun/ruby_of_aja: Rubyプログラミング学習用の練習問題管理アプリケーション

Rubyプログラミングの練習問題を管理するアプリケーションです。 問題文はMarkdownで書けます。 問題の解答をPostするとサーバサイドで実行して正解か不正かを判定する機能があります。 テストデータをどうやって書こうかな?と悩んだあたりで止まりました。

2021年はなにか形があるものを残したいですね。

2020年のプルリクエストをふりかえる

2020年のプルリクエストを振り返る - kakakakakku blogを参考にしました。 あまりにも少なかったのでissueも入れました。 次の条件でGithubを検索します。

is:public author:ledsun created:2020

Search · is:public author:ledsun created:2020 · GitHub

f:id:ledsun:20201229104051p:plain
2020年のIssueとPull Request

2番目のIssueが解決済みなことを思い出したので、閉じておきました。ふりかえりは大事ですね。

rbenvでRuby 3.0を使う

Homebrewをインストールし直す - @ledsun blogrbenvをgitから入れました。

git pullします。

~ cd .rbenv/
~ git pull --ff-only
Already up to date.

んー、rbenv install --list-allしても3.0.0が出てきません。

~ cd ~/.rbenv/plugins/ruby-build
~ git pull --ff-only

が、必要でした。

~ ruby --version
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin18]

続 Gitのdiffを振り返る

Gitのdiffを振り返る - @ledsun blog でGitの記録から、約2年間の毎日のファイルの変更行数を見ました。 1日に4000行の人力では不可能に思える変更が見つかりました。 今度は巨大な変更を含むコミットを探してみましょう。

次のRubyスクリプトを使います。

require 'date'

EXCLUDE = "':(exclude)package-lock.json' ':(exclude)*.min.js' ':(exclude)*.css' ':(exclude)dist/*' ':(exclude)dev/vender' ':(exclude)src/lib/modules' ':(exclude)*.md'"

start_date = Date.parse $*.shift
end_date = Date.parse $*.shift
limit = $*.shift.to_i

git_rev_list_command = "git rev-list --after='#{start_date} 00:00' --before='#{end_date} 23:59' current"
p git_rev_list_command

revs = `#{git_rev_list_command}`.split("\n")
revs.reverse.each do |revision|
  result_of_git_show = `git show --shortstat --oneline #{revision} #{EXCLUDE}`
  short_stat = result_of_git_show.split("\n")[1]
  next unless short_stat

  insertions, deletions = short_stat.split(",")[1..].map { _1.to_i}
  next unless insertions
  next unless deletions

  if insertions > limit || deletions > limit
    system "git show #{revision} --format=format:'%H%n%ai%n%s' --shortstat #{EXCLUDE}"
    puts "\n"
  end
end

実行してみると、次の結果が得られました。 1000行以上の変更をしているコミットを探しました。

~ ruby gitdiffindate.rb 2019/05/22 2020/12/31 1000
"git rev-list --after='2019-05-22 00:00' --before='2020-12-31 23:59' current"
663c84dbf80a105b22078418f87c2474ac9487a5
2019-07-24 14:58:23 +0900
code-style: Introduce the Prettier code formatter
 330 files changed, 3990 insertions(+), 1883 deletions(-)

5c3f2c2122f676df8f91a03df5ef12830082cb93
2019-07-24 15:01:49 +0900
code-style: Ban one-var
 56 files changed, 1077 insertions(+), 1086 deletions(-)

cf2acd9f917e2652048df97050c0e885aaec0731
2019-11-12 11:18:55 +0900
refactor: Format annotation files
 10 files changed, 2543 insertions(+), 2158 deletions(-)

d5c2b8881c9edbececee8b7753a8f04b2dd3f5cd
2019-11-19 15:35:29 +0900
refactor: Format less files
 6 files changed, 1181 insertions(+), 968 deletions(-)

5bb834fca322388da6b3ca94db99e4910f6f774a
2019-11-07 20:28:24 +0900
feature: Show attribute tabs in the Pallet
 25 files changed, 1180 insertions(+), 69 deletions(-)

発見したコミットログを見ると、ほとんどがツールを使ったフォーマット変更でした。

feature: Show attribute tabs in the Palletは、機能追加です。 機能追加とはいえ、1コミットで1000行入れるのは、我ながらよくなかったと反省です。

この5つのコミットが2019-07-292019-12-02に集計された結果、1日4000行オーバーになっていました。 つまりノイズです。これらの異常値を除いてグラフを作り直してみます。

f:id:ledsun:20201227231409p:plain
異常値を除いた一日毎のファイルの変更行数

今度は2000行近い変更が異常値に思えてきました。

Gitのdiffを振り返る

Gitのコミット数を振り返る - @ledsun blogで、Gitのコミット数をふりかえりました。 今年はコミットの粒度を変えたためコミット数が増えています。 その結果、作業効率は上がったのでしょうか?今度はGitのdiffをふりかえってみます。

日付単位でgit diffのsohrtstatをとります。

3 files changed, 8 insertions(+), 3 deletions(-)

こんなやつです。

次のRubyスクリプトを使いました。

require 'date'

base_date = Date.new(2019, 5, 22)
0.upto(585) do |i|
  target_date = base_date + i

  start_commit = `git rev-list -1 --before="#{target_date} 00:00" current`.chomp
  end_commit = `git rev-list -1 --before="#{target_date} 23:59" current`.chomp

  # An expected output string format is like " 3 files changed, 8 insertions(+), 3 deletions(-)".
  stats = `git diff --shortstat #{start_commit} #{end_commit} ':(exclude)package-lock.json' ':(exclude)*.min.js' ':(exclude)*.css' ':(exclude)dist/*' ':(exclude)dev/vender' ':(exclude)src/lib/modules' ':(exclude)*.md'`
  parsed_stats = stats.split(",").map { _1.to_i }
  if parsed_stats.size == 3
    puts [target_date].concat(parsed_stats).concat([start_commit, end_commit]).join(" ")
  else
    puts [target_date, 0, 0, 0].join(" ")
  end
end

gitのcommitの日付を元にしています。 git rebaseなどで順序が入れ替えていると、本当にその日付にコミットしたとは限りません。 全体的な傾向がわかればいいので、ここでは無視します。

gunplotでグラフ化します。

gnuplot> set xdata time
gnuplot> set timefmt "%Y-%m-%d"
gnuplot> plot 'daily_git_stast.dat' using 1:3 with steps title 'insertions', '4col.csv' using 1:4 with steps title 'deletions'

f:id:ledsun:20201226100949p:plain
daily git diff

4000行を超える変更をした日が二つあります。

Gitのコミット数を振り返る

次のRubyスクリプトを使って、あるリポジトリの2020年のGitコミット数を月単位で集計します。

require 'date'

1.upto(12) do |i|
  start_date = Date.new(2020, i, 1)
  end_date = (start_date >> 1) - 1

  commits = `git log --oneline --since #{start_date} --until #{end_date} | wc -l`
  puts "#{i}, #{commits.chop.to_i}"
end

結果は次のとおりです。

1, 60
2, 76
3, 32
4, 45
5, 19
6, 53
7, 47
8, 57
9, 138
10, 482
11, 432
12, 432

gnuplotを使ってグラフにします。

gnuplot> plot "./commits.dat" with line

f:id:ledsun:20201220211139p:plain
2020年の月別コミット数

10月からコミット数が通常の3倍に増えていました。

参考