Rails4, Rails5の時間の足し算の挙動の違い

経緯

Rails4で、日付の足し算の挙動が自分の期待と異なっていたので調査した。 1/31の翌月の翌月も、1/31の二ヶ月後も、どちらも2018/3/31になるのを期待していたが、実際の結果は異なった。 前者は「1/31の翌月の翌月」 => 「2/28の翌月」 => 「3/28」と処理されているもよう。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Wed, 28 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018

後に、Rails5でも確認したところ、挙動が変更され、自分の期待と同様に動くことを確認した。

結論

Rails5なら、自分の期待通りに動く。 Rails4のときは、 Date.new + x.month + y.momnthDate.new + (x + y).month は計算結果が異なることがあるので、足し方に注意。

調査環境

Rails4, Rails5での挙動は、下記の環境で確認した。

# Rails4環境
~/t/rails4-sandbox (master|✔) $ docker-compose exec web rails --version
Rails 4.2.10
~/t/rails4-sandbox (master|✔) $ docker-compose exec web ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
~/t/rails4-sandbox (master|✔) $ docker-compose exec db  psql -U postgres -c "SELECT version()"
                                         version

--------------------------------------------------------------------------------
----------
 PostgreSQL 9.6.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.
2, 64-bit
(1 row)
# Rails5環境
~/t/rails-sandbox (master|✔) $ docker-compose exec web rails --version
Rails 5.2.1
~/t/rails-sandbox (master|✔) $ docker-compose exec web ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
~/t/rails-sandbox (master|✔) $ docker-compose exec db  psql -U postgres -c "SELECT version()"
                                         version

------------------------------------------------------------------------------------------
 PostgreSQL 9.6.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.2, 64-bit
(1 row)

調査メモ

問題の挙動は下記の通り。 どちらも「二ヶ月後」の3/31になると期待していた。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Wed, 28 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018

Rails5では自分の期待通りに動くようになっている。

Loading development environment (Rails 5.2.1)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Sat, 31 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018
irb(main):004:0>

Rails4での、ActiveRecord::Durationどうしの足し算の処理方法は、下記の通り。

# https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/duration.rb#L16-L24
    # Adds another Duration or a Numeric to this Duration. Numeric values
    # are treated as seconds.
    def +(other)
      if Duration === other
        Duration.new(value + other.value, @parts + other.parts)
      else
        Duration.new(value + other, @parts + [[:seconds, other]])
      end
    end

Rails4では、Durationどうしの足し算は、@parts部分は、じつは足していない。配列で足すべき要素を覚えているだけ。試しに実行してみると以下のようになる。

Loading development environment (Rails 4.2.10)
irb(main):001:0> (1.month + 1.month).parts
=> [[:months, 1], [:months, 1]]
irb(main):002:0> 2.month.parts
=> [[:months, 2]]

Rails5のDurationどうしの足し算処理の内容は下記の通り。@parts部分はHash。自分の期待通りに足している。

# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L234-L247
    # Adds another Duration or a Numeric to this Duration. Numeric values
    # are treated as seconds.
    def +(other)
      if Duration === other
        parts = @parts.dup
        other.parts.each do |(key, value)|
          parts[key] += value
        end
        Duration.new(value + other.value, parts)
      else
        seconds = @parts[:seconds] + other
        Duration.new(value + other, @parts.merge(seconds: seconds))
      end
    end

実行結果。

Loading development environment (Rails 5.2.1)
irb(main):001:0> (1.month + 1.month).parts
=> {:months=>2}
irb(main):002:0> 2.month.parts
=> {:months=>2}

Date + Durationの処理は、Duration#sumが呼ばれる。

parts.injectで、足す単位(seconds, minutesなど)に応じてDate#advance || Date#sinceしている。

minutes, hoursの場合,Rails4ではadvanceだったのが、Rails5では秒換算でsinceされる。なぜだろう?

advenceされるのは、後のadvanceのコードを見ればわかるとおり、years, months, weeks, daysの場合だ。

# Rails4
# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/duration.rb#L133-L145
      def sum(sign, time = ::Time.current) #:nodoc:
        parts.inject(time) do |t,(type,number)|
          if t.acts_like?(:time) || t.acts_like?(:date)
            if type == :seconds
              t.since(sign * number)
            else
              t.advance(type => sign * number)
            end
          else
            raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
          end
        end
      end
# Rails5
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L402-L418
      def sum(sign, time = ::Time.current)
        parts.inject(time) do |t, (type, number)|
          if t.acts_like?(:time) || t.acts_like?(:date)
            if type == :seconds
              t.since(sign * number)
            elsif type == :minutes
              t.since(sign * number * 60)
            elsif type == :hours
              t.since(sign * number * 3600)
            else
              t.advance(type => sign * number)
            end
          else
            raise ::ArgumentError, "expected a time or date, got #{time.inspect}" 
          end
        end
      end

Date#sinceは以下の処理。Rails4もRails5も同じ。

# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/core_ext/date/calculations.rb#L57-L62
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/core_ext/date/calculations.rb#L59-L64
  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  # and then adds the specified number of seconds
  def since(seconds)
    in_time_zone.since(seconds)
  end
  alias :in :since

。。 Date#advanceは以下の処理。Rails4もRails5も同じ。

# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/core_ext/date/calculations.rb#L108-L118
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/core_ext/date/calculations.rb#L110-L120
def advance(options)
  options = options.dup
  d = self
  d = d >> options.delete(:years) * 12 if options[:years]
  d = d >> options.delete(:months)     if options[:months]
  d = d +  options.delete(:weeks) * 7  if options[:weeks]
  d = d +  options.delete(:days)       if options[:days]
  d
end

秒数換算(1ヶ月30日)の@valuesと、seconds, months, weeks, daysなどの人間に優しい値?の@partsを持っている。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month.instance_variables
=> [:@value, :@parts]
irb(main):002:0> 1.month.instance_variable_get(:@value)
=> 2592000
irb(main):003:0> 1.month.instance_variable_get(:@parts)
=> [[:months, 1]]
irb(main):004:0> (1.month + 2.days + 3.month).instance_variable_get(:@value)
=> 10540800
irb(main):005:0> (1.month + 2.days + 3.month).instance_variable_get(:@parts)
=> [[:months, 1], [:days, 2], [:months, 3]]
irb(main):006:0> d = 1.month + 1.month.value
=> 1 month and 2592000 seconds
irb(main):007:0> 1.month.instance_variables
=> [:@value, :@parts]
irb(main):008:0> d.instance_variable_get(:@value)
=> 5184000
irb(main):009:0> d.instance_variable_get(:@parts)
=> [[:months, 1], [:seconds, 2592000]]
irb(main):010:0> d.value
=> 5184000
Loading development environment (Rails 5.2.1)
rb(main):001:0> 1.month.instance_variables
=> [:@parts, :@value]
irb(main):002:0> 1.month.instance_variable_get(:@value)
=> 2629746
irb(main):003:0> 1.month.instance_variable_get(:@parts)
=> {:months=>1}
irb(main):004:0> (1.month + 2.days + 3.month).instance_variable_get(:@value)
=> 10691784
irb(main):005:0> (1.month + 2.days + 3.month).instance_variable_get(:@parts)
=> {:months=>4, :days=>2}
irb(main):006:0> d = 1.month + 1.month.value
=> 1 month and 2629746 seconds
irb(main):007:0> 1.month.instance_variables
=> [:@parts, :@value]
irb(main):008:0> d.instance_variable_get(:@value)
=> 5259492
irb(main):009:0> d.instance_variable_get(:@parts)
=> {:months=>1, :seconds=>2629746}
irb(main):010:0> d.value
=> 5259492

1.yearは360日でも365日でもない。365.25日。1.yearと1.monthのvalue換算の値の大きさが、Rails4とRails5で異なっている。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.year.value
=> 31557600.0
irb(main):002:0> 1.month.value * 12
=> 31104000
irb(main):003:0> 1.day.value * 365
=> 31536000

Rails4での各単位の秒数については下記の通り。

# Rails4
# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/duration.rb#L59-L81
   # Returns the number of seconds that this Duration represents.
    #
    #   1.minute.to_i   # => 60
    #   1.hour.to_i     # => 3600
    #   1.day.to_i      # => 86400
    #
    # Note that this conversion makes some assumptions about the
    # duration of some periods, e.g. months are always 30 days
    # and years are 365.25 days:
    #
    #   # equivalent to 30.days.to_i
    #   1.month.to_i    # => 2592000
    #
    #   # equivalent to 365.25.days.to_i
    #   1.year.to_i     # => 31557600
    #
    # In such cases, Ruby's core
    # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
    # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
    # date and time arithmetic.
    def to_i
      @value.to_i
    end

Rails5での各単位の秒数については下記の通り。 1ヶ月は30日でなく、1年の12分の1に変更されている。

# Rails5.2
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L322-L344
    # Returns the number of seconds that this Duration represents.
    #
    #   1.minute.to_i   # => 60
    #   1.hour.to_i     # => 3600
    #   1.day.to_i      # => 86400
    #
    # Note that this conversion makes some assumptions about the
    # duration of some periods, e.g. months are always 1/12 of year
    # and years are 365.2425 days:
    #
    #   # equivalent to (1.year / 12).to_i
    #   1.month.to_i    # => 2629746
    #
    #   # equivalent to 365.2425.days.to_i
    #   1.year.to_i     # => 31556952
    #
    # In such cases, Ruby's core
    # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
    # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
    # date and time arithmetic.
    def to_i
      @value.to_i
    end
Loading development environment (Rails 5.2.1)
irb(main):001:0> 1.year.value
=> 31556952
irb(main):002:0> 1.month.value * 12
=> 31556952
irb(main):003:0> 1.day.value * 365
=> 31536000
irb(main):004:0>

余談:SQL

SQLでも、「二ヶ月間 + 一ヶ月間」と「一ヶ月+二ヶ月間」では、結果が違うことがある。

psql (9.6.5)
Type "help" for help.

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 2) + interval '1 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-30 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1) + interval '2 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1) + make_interval(months := 2)) AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  interval '1 month' + interval '2 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

以下の様に書くと、つねに(私の)期待どおり動作する。

psql (9.6.5)
Type "help" for help.

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1 + 2));
    date    |      ?column?
------------+---------------------
 2017-01-31 | 2017-04-30 00:00:00
(1 row)

docker-composeを環境別に使い分けたい

経緯

もともと、docker-compose.ymlで開発環境を用意していた。 プレビュー環境構築にあたり、諸々の事情により、プレビュー環境では別途新規に書き起こされたdocker-compose.yml、Dockerfileを使うことになった。 既存の設定は開発環境で使いつつ、プレビュー環境では新規の設定を使いたい。

やりたいこと

以下のような配慮をしつつ、Docker環境を使い分けたい。

  • 開発用PCでは、開発用docker-composeとプレビュー用docker-composeを適宜使い分けられるようにしたい。
  • プレビュー用ホストではプレビュー用docker-composeのみを使いたい。
  • プレビュー用ホストでうっかり開発用docker-composeで立ち上げることは絶対にしたくない。フールプルーフにしたい。
  • 開発PCでうっかりプレビュー用docker-composeを立ち上げてしまうのも避けたい。逆よりはマシだが。

実現案

  1. docker-composeのファイル名を分ける(例: docker-compose.development.yml)
    1. メリット
      1. フールプルーフ
      2. ファイル名を見れば用途がわかりやすい
    2. デメリット
      1. docker-compose -fオプションでいちいち指定するのが面倒
        1. だからといってよくつかう環境用の設定ファイルの名前をdocker-compose.ymlにすると、他の環境で使用してしまうというオペミスが発生する
        2. 環境変数COMPOSE_FILEで使用するdocker-compose.ymlを指定すれば対処できる 参考: suin.io
          1. メリット
            1. 一度設定すれば-fオプションが不要
          2. デメリット
            1. 環境構築時、知らない/忘れたひとは-fオプション無しで起動してハマる
              1. README.mdに書いておけばいいのでは
            2. 特定ディレクトリ下で環境変数を設定するいいかんじの方法が、自分はまだわからない => dotenvがよさそう。他の開発者と共有しやすそうなので。
              1. dotenv
              2. bashrcに記述 https://stackoverflow.com/a/14463040
              3. tmuxinator
  2. ブランチを分ける。追加開発分は適宜プレビュー用ブランチにマージする
    1. メリット
      1. -fオプションが不要
      2. 知らないひとがとりあえずdocker-compose upしてイライラすることもない
    2. デメリット
      1. フールプルーフでもフェイルセーフでもない。知らないで適当にdevelopブランチをプルしてきてdocker-compose upするとまずい
        1. previewで動かすとき、developブランチで合ってるのかな?というのは普通に気にするよね?
      2. docker-compose.ymlを変更したとき、おそらく変更がマージされてしまう
  3. 差分の設定を上書く
    1. メリット
      1. 公式の安心感 Use Compose in production
      2. ブランチを分ける必要がない
    2. デメリット
      1. フールプルーフでもフェイルセーフでもない。うっかりするとdevelopmentモードで起動してしまう
      2. docker-compose設定の全体像を把握しづらそう
      3. docker-compose設定を変更しづらそう
      4. 今回のケースだと、docker-compose設定が違いすぎるので、向かなさそう
  4. 環境ごとにわけない
    1. メリット
      1. 環境が分かれていることによるミスが発生しない
      2. じつは公式にも似たようなことが書いてある (The easiest way to deploy an application is to run it on a single server, similar to how you would run your development environment. If you want to scale up your application, you can run Compose apps on a Swarm cluster.)
    2. デメリット
      1. 当然、環境別の変更設定ができない

とりあえずの結論

1つ目の案でやってみる。フールプルーフで、かつ環境ごとに変更できるため。環境変数はdotenvを使って設定する。

Arch Linuxでvim8でdeoplete.nvimがすんなり動かなかった

やりたいこと

vim8でdeoplete.nvimを動かす

やったこと

期待してたこと

  • vim起動時にエラーが出ない
  • neocomplete.vimを使ってたときのように、適宜補完が効く

実際に起こったこと

  • vim起動時、以下のエラーが出る
[vim-hug-neovim-rpc] failed executing: pythonx import neovim
[vim-hug-neovim-rpc] Vim(pythonx):Traceback (most recent call last):
続けるにはENTERを押すかコマンドを入力してください`

解決法

pip install neovimのかわりに、 pacman -S python-neovim すればいいのかな? まったく自信ないです。

参考

調査メモ(読む価値ないです)

python全くわからんが、一応パッケージはインストールされてるっぽい?

~ $ python
Python 3.6.5 (default, Apr 14 2018, 13:17:30) 
[GCC 7.3.1 20180406] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import neovim
>>> import greenlet
>>>

vimでもpython認識してるっぽい

:echo('pythonx') #=> 1
:echo('python3') #=> 1
:echo('python') #=> 1
:echo('python2') #=> 0

vimでgreenletが読み込めない。 ファイル開いたらバイナリっぽかった

:pythonx import neovim
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib/python3.6/site-packages/neovim/__init__.py", line 11, in <module>
    from .msgpack_rpc import (ErrorResponse, child_session, socket_session,
  File "/usr/lib/python3.6/site-packages/neovim/msgpack_rpc/__init__.py", line 10, in <module>
    from .session import ErrorResponse, Session
  File "/usr/lib/python3.6/site-packages/neovim/msgpack_rpc/session.py", line 6, in <module>
    import greenlet
ImportError: /usr/lib/python3.6/site-packages/greenlet.cpython-36m-x86_64-linux-gnu.so: undefined symbol:
 PyExc_ValueError

ほかの普通の?パッケージだとimportできるっぽい

 :pythonx import pycurl #=> エラー出ない

https://bbs.archlinux.org/viewtopic.php?id=232873を見ると、greenletはpacman経由でも入れることが可能で、そちらだと動くっぽい?

sudo pacman -S python-greenlet
:pythonx import greenlet #=> エラー出ない

neovimをpipで入れて、依存してるgreenletはpacmanで入れるって、大丈夫なのか?

なんかpacmanのリポジトリにpython-neovimってあるな。それ入れればいいか。

しかし、既存ディレクトリと衝突し、インストールできないとのこと。ログは以下の通り。

~ $ sudo pacman -S python-neovim
依存関係を解決しています...
衝突するパッケージがないか確認しています...

パッケージ (4) neovim-0.2.2-5  python-greenlet-0.4.13-1
               python-msgpack-0.5.6-1  python-neovim-0.2.6-1

合計インストール容量:  18.82 MiB

:: インストールを行いますか? [Y/n] Y
(4/4) キーリングのキーを確認                       [##########] 100%
(4/4) パッケージの整合性をチェック                 [##########] 100%
(4/4) パッケージファイルのロード                   [##########] 100%
(4/4) ファイルの衝突をチェック                     [##########] 100%
エラー: 処理を完了できませんでした (衝突しているファイル)
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__init__.py がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/__init__.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/_version.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/exceptions.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/fallback.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/_packer.cpython-36m-x86_64-linux-gnu.so がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/_unpacker.cpython-36m-x86_64-linux-gnu.so がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/_version.py がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/exceptions.py がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/fallback.py がファイルシステムに存在しています
エラーが発生したため、パッケージは更新されませんでした。

メモにとってないが、いままでの調査中に、pipやらpacmanやらでneovimとgreenletを入れたり消したりしたような気がしている。衝突の原因はそれな気がする。

どういう副作用が出るかまったくわからないが、/usr/lib/python3.6/site-packages/msgpackディレクトリをmsgpack.bkにリネームした。おすすめしません。その後sudo pacman -S python-neovimしたらうまくいった。

差分は以下の通り。なんか違うね。

/u/l/p/site-packages $ diff msgpack msgpack.bk
共通のサブディレクトリー: msgpack/__pycache__ と msgpack.bk/__pycache__
バイナリーファイル msgpack/_packer.cpython-36m-x86_64-linux-gnu.so とmsgpack.bk/_packer.cpython-36m-x86_64-linux-gnu.so は異なります
バイナリーファイル msgpack/_unpacker.cpython-36m-x86_64-linux-gnu.so とmsgpack.bk/_unpacker.cpython-36m-x86_64-linux-gnu.so は異なります

最後に念の為入れ直す。

sudo pacman -R python-neovim
sudo pacman -R python-greenlet
sudo pacman -S python-neovim

(いま知ったが、pacman -Rには--recursiveオプションがある。sudo pacman -Rs <package>のほうがよかったか。)

以上で完了。vimは期待通り動く。なんかいろいろゴミを残している気がする。でももう面倒だからいいや。

rubocopでrubyコードのベストプラクティスを学ぶ(Rails + Visual Studio Code)

以下の文章はこんな方を想定しています

vscodeでrailsを書いている。 自分のコードをもっと「いいコード」にしたい。が、指導してくれる人間は周囲にいない。

rubocop

github.com

rubyコードの静的解析を行うGem. ベストプラクティスに従っていない箇所を指摘してくれる。 ベストプラクティスについてはデフォルトで設定されている。変更もできる。

インストール

公式のインストール方法は以下のとおり。

github.com

自分はbundlerを使いインストールした。

group :development, :test do
+  # コードの静的解析ツール
+  gem 'rubocop', require: false
end
bundle install

以下で解析が走る。

rubocop

いい感じの解析ルールを設定する

ネットを見る感じ、デフォルトのルールはそのまま使っているひとはあまりいなさそう。厳しすぎるらしい。

しかし、自分で設定する気は起きない… @onk さんの設定を真似させていただくことにした。

github.com

group :development, :test do
   # コードの静的解析ツール
   gem 'rubocop', require: false
+  # rubocopの解析ルール設定
+  gem 'onkcop', require: false
 end
bundle install
bundle exec onkcop init

自動生成されたファイルを編集する

 inherit_gem:
   onkcop:
     - "config/rubocop.yml"
     # uncomment if use rails cops
-    # - "config/rails.yml"
+    - "config/rails.yml"
     # uncomment if use rspec cops
-    # - "config/rspec.yml"
+    - "config/rspec.yml"

 AllCops:
   TargetRubyVersion: 2.5
   # uncomment if use rails cops
-  # TargetRailsVersion: 5.1
+  TargetRailsVersion: 5.1
rubocop

で走る。解析ルールがrubocopデフォルトから変更されていることを確認する。自分の場合、

- 59 files inspected, 284 offenses detected
+ 58 files inspected, 216 offenses detected

と変化した。(このあたり、間にほかの操作を入れたかもしれません。とりあえず、vimとかでシングルクオートで文字列リテラルを宣言したときに、『[Style/StringLiterals] Style/StringLiterals: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.』と怒られるようになっていれば、onkcopのルールが適用されているはずです。)

プロジェクトで動かしてみる

とりあえず試す

bundle exec rubocop で解析結果が出力される。自分の場合、たしか数百件のアラートが出た。アラートが大量に出るので面食らうが、ほとんどは自動修正できるものか、メトリクス関連(メソッドが長すぎる、など)だった。手動ですぐに直すべき箇所は少ないので、ビビらないでいいと思う。

自動修正する

bundle exec rubocop --auto-correct で、自動修正できるものはしてくれる。自分の場合、アラートは半分以下に減った。修正のほとんどは空白文字関連。自分が面白いと思ったのは以下の自動修正。

&:symbolによる圧縮
users.map{|user| user.id}
=>
users.map(&:id)
if文(というか返り値があるので式)の返り値を使って代入
if condition
  @user = foo
else
  @user = bar
end
=>
@user = if condition
          foo
        else
          bar
        end
ミュータブルな定数のフリーズ
SOME_CONSTS = %w[aaa bbb ccc]
=>
SOME_CONSTS = %w[aaa bbb ccc].freeze

(注: rubyの定数は再代入が可能)

【2018/02/20追記 Pocke様からご指摘をいただき修正】

freezeすることで、オブジェクトの変更ができなくなります。 (フリーズしても、定数への再代入は引き続き可能です。)

以下、freezeについて調べたメモ。

# 配列をフリーズ
NAMES = %w[yamada sato].freeze
=> ["yamada", "sato"]

# 配列への変更はできない
NAMES.sort!
#=> FrozenError (can't modify frozen Array)

# 配列をfreezeしても、配列の要素の変更はできる
NAMES.map!(&:upcase)
#=> FrozenError (can't modify frozen Array)
NAMES.map(&:upcase!)
=> ["YAMADA", "SATO"]
NAMES
=> ["YAMADA", "SATO"]

# 中身もフリーズすれば、上の操作も防げる
OTHER_NAMES = %w[takahasi akiyama].map(&:freeze).freeze
=> ["takahasi", "akiyama"]
OTHER_NAMES.map!(&:upcase)
FrozenError (can't modify frozen Array)
OTHER_NAMES.map(&:upcase!)
FrozenError (can't modify frozen String)

# freezeしても再代入はできる
OTHER_NAMES = ["bukkowasu"]
(irb):12: warning: already initialized constant OTHER_NAMES
(irb):8: warning: previous definition of OTHER_NAMES was here
=> ["bukkowasu"]
OTHER_NAMES
=> ["bukkowasu"]

【2018/02/20追記 Pocke様からご指摘をいただき修正 おわり】

残りを適宜手動で直す

bundle exec --auto-gen で、解析結果のまとめが.rubocop_todo.ymlに出力される。内容を見て、すぐ直せるものは修正していく。 メトリクス系(メソッドが長すぎる、等)はおいおい修正したい。

vscodeの設定

vscodeでもrubocop && onkcopが効くように設定する。

プラグインをインストール

以下の公式の記述に従い、Rubyプラグインをインストールする。

marketplace.visualstudio.com

設定ファイルを作成

vscodeの設定にはスコープがふたつある。ユーザーとワークスペースだ。 たとえばプロジェクトごとに設定を分けたい場合は、ワークスペース設定を行う。 わけなくていい場合はユーザー設定を行う。

以下に従い、ワークスペース設定を開く。

code.visualstudio.com

ruby language settingsを見つける。「設定の検索」欄に『ruby』と入れるとかんたんに絞りこめる。

ワークスペースを適宜設定する。設定は以下に記載がある。

marketplace.visualstudio.com

自分は以下のようにした。

{
    "ruby.lint": {
        "rubocop": true
    }
}

code.visualstudio.com

vscodeを再起動してrubyファイルを開くと、規約に従っていない箇所に緑の波線が入る。たしかにrubocopのチェックが走ることを確認した。指摘のでないかたは、完璧に従っているコードを書いている懸念があるので、おかしなコードをかいてみてください:)

自分はシステムには入れず、プロジェクトのvendor/bundle以下にrubocopを入れているが、特にそれを明示せずともうまく動いた。

bashの特殊ファイル(.bash_profile, .bashrcとか)のメモ

.bash_profileと.bashrcの違いについてまたグーグル検索。もう何回目だ…

備忘のためにメモしておく。 『入門bash』3章、「環境のカスタマイズ」を参考にしました。

以下3つのファイルを意識しておけばよさそう。ユーザーごとの設定。

  • .bash_profile …ログイン時に読み込まれる
  • .bash_logout …ログアウト時に読み込まれる
  • .bashrc …新しいシェルの起動時に読み込まれる

以下のファイルも特殊ファイルとして認識されるが、自分はあまり使う機会はなさそう。これもユーザーごとの設定。 ログイン時に.bash_profile, .bash_login, .loginの順に検索される。

  • .bash_login …Cシェル由来
  • .profile …Bourneシェル、Kornシェル由来

(.profileについては、他のシェルとの移行|共存がかんたんだよ、ということかな。.bash_loginはCシェル利用者に設計が理解しやすいというだけか?自分はこれらのシェルの設定ファイルを使わないので不要な知識…)

以下はシステム全体の設定。あまり変更することはないだろう。

  • /etc/profile …ログイン時に読み込まれる

理解が不安な、.bash_profile, .bashrc, /etc/profileの挙動を確認してみた。シャープイコール大なり(#=>)の行はコメント。

#=> まずは.bash_profile, .bashrcの確認
$ echo echo .bash_profile is loaded. >> ~/.bash_profile
$ echo echo .bashrc is loaded. >> ~/.bashrc
#=> bashrc, bash_profileにechoを仕込む。
$ echo $SHELL
/usr/bin/fish
#=> ふだんはfishがログインシェル
$ bash
#=> インタラクティブだがログインシェルではないbashを起動。.bashrcが読まれるはず。
.bashrc is loaded.
#=> 期待通り。
$ bash
#=> bash内でさらにbashを起動してみる。
.bashrc is loaded.
#=> もう一度読まれる。まあそうか。
$ ps --forest
#=> プロセスはこんな状態。
  PID TTY          TIME CMD
18565 pts/0    00:00:01 fish
21358 pts/0    00:00:00  \_ bash
21368 pts/0    00:00:00      \_ bash
21385 pts/0    00:00:00          \_ ps
$ exit
$ exit
$ bash --login
#=> ログインシェルとしてbashを起動。.bash_profileが読まれるはず。
.bash_profile is loaded.
#=> そのとおり。
$ ログアウト
$ echo source .bash_profile >> .bashrc
#=> .bashrcが読まれたら、.bash_profileも読まれるようにする。非ログインシェルでも、ログインシェル立ち上げ時に読まれるコードも読まれるようになる。
$ bash
.bashrc is loaded.
.bash_profile is loaded.
$ echo source .bashrc >> .bash_profile
#=> .bash_profileが読まれたら、.bashrcも読まれるようにする。無限ループになるはず
$ exit
$ bash --login
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
...
...
#=> 期待通り無限ループ。
^C$ ログアウト
$ vim .bash_profile
$ vim .bashrc
#=> sourceの行を削除して、無限ループにならないようにする。
#=> 最後に/etc/profileの挙動を確認する
$ echo 'echo etc/profile is loaded.' | sudo tee --append /etc/profile
#=> /etc/profile に書き込み。sudo権限が必要なため、このような書き方に
$ bash --login
etc/profile is loaded.
.bash_profile is loaded.
$ ログアウト
$ sudo vim /etc/profile
#=> 片付け。さっきのechoを消しておく
$ vim .bashrc .bash_profile
#=> 片付け。echoを消す

以上

リファクタリング:Rubyエディション 第一章 写経してみた

初めて写経というものをやってみた。

ただ本を読むよりも、内容を深く理解できると感じた。

写経すると能動的な頭の使い方になって、内容を深く咀嚼できるのかな。

もちろん普通に読むより時間がかかる。当面は重要な本に絞って写経するようにしたい。

写経の方法は、以下のツイートを参考にさせていただいた。

作成したリポジトリを試しに公開してみる。もしほかの方にとって利用価値があるとすれば、テストをRSpecで書いているという点と、個人的に誤記に見える箇所を記録しているという点だと思う。(自分には、公式の正誤表を見つけられませんでした。ご存じの方はご指摘頂けると助かります。)

github.com

Node.js, npm, electron

Node.js, npm, Electronについて、言葉だけ知っていたが内容や関連がわからなかったので調べてみた。

端的に言うと以下のとおりだと認識した。

JSのサーバーサイド実行環境がNode.js。 Node.jsのパッケージマネージャーがnpm。 有名パッケージのひとつがelectron。 electronを使うと、マルチプラットフォームのデスクトップアプリがかんたんに作れる。

以下、もう少し詳しく書く。

Node.jsってなに?

JavaScriptの実行環境。サーバーサイドでJSが動かせるようになる。

$ node
> console.log('hello world!');
hello world!
undefined
> ⏎

npmってなに?

Node Package Manager。 公開されているJSパッケージをかんたんに取り込める。 パッケージの配布もできる。(自分の場合はインストールの利用が主だろう。) さまざまなパッケージが登録されている。"the world’s largest software registry."とのこと。

npmのコマンドはどんなのがあるの?

このあたりが気になった。

npm -l # display full usage info
npm config list
npm run <command> # Run arbitrary package scripts
npm ls # List installed packages

例えばどういうパッケージがあるの?

electron(デスクトップアプリ作成), Grunt(JSタスクランナー), Webpack(モジュールバンドラー)

electronについてもう少しくわしく

JSでマルチプラットフォームのデスクトップアプリが作れるようになるパッケージ。 メインプロセスでうウィンドウを制御する。 各ページはレンダラープロセスが担う。

メインプロセスってなに?

プログラムのエントリーポイント。main.jsとかindex.jsという名前であることが多い。 例えば、requireされるときにこのファイルの返り値が返される。詳細は下記の通り。 docs.npmjs.com

試しに、インストールしたモジュールの中を覗いてみた。

github.com

上記のelectron-quick-startでnpm installしてから、 適当にelectron-downloadモジュールの中を見てみる。 electron-quick-start/node_modules/electron/package.json では "main": "build/index.js", と記載されていた。 そして、 electron-quick-start/node_modules/electron-download/build/index.js はたしかに存在していた。

npmに対抗馬はいないの?

yarnがある。npmの欠点を解消すべく作られた。GoogleFacebookのひとたちも参加してるらしい。 www.webprofessional.jp

余談。yarnは、デフォルトでバージョンのロックファイルを生成する、というのをウリのひとつにしている。 npmでは自動で生成されないということを示唆していると思うのだが、electron-quick-startをnpm installしたら、package.json.lockが生成された。 どういうことだろう…npmも自動生成するようになったのか?node, npmのバージョンは下記の通り。

$ npm --version
5.5.1
$ node --version
v9.2.0

以上