Quantcast
Channel: TechRacho
Viewing all 2990 articles
Browse latest View live

Ruby: eachよりもmapなどのコレクションを積極的に使おう(社内勉強会)

$
0
0
  • 2018/07/20: 公開
  • 2020/11/04: 細部を更新

こんにちは、hachi8833です。BPS社内勉強会でのkazzさんのスライドを元にした記事をお送りいたします。

RubyのEnumerableのコレクション系メソッドのいくつかを合間合間に再実装しながら進める構成になっています。

⚓ Rubyのforは原則使わないこと

Rubyである程度書けるようになれば、ループでforを使う人はまずいないと思います。Rubyスタイルガイド↓でも「2-07【統一】forは原則使わない」とあります。

Rubyスタイルガイドを読む: 文法(2)アンダースコア、多重代入、三項演算子、if/unless

# forの場合(まず使わない)
list = (1..5).to_a.freeze

for element in list
  puts "forによる表示: #{element}"
end
# eachの場合
list = (1..5).to_a.freeze

list.each do |element|
  puts "eachによる表示: #{element}"
end

⚓ 参考: #eachforで実装して理解する

上のスタイルガイドにも「forの内部実装は#each」と記載されていますが、せっかくなので#eachforで実装してみましょう。なおRubyのforはキーワードですが、#eachはメソッドです。

参考: Array#each (Ruby 2.7.0 リファレンスマニュアル)

# オレオレeach
def my_each(collection, &block)
  for element in collection
    yield(element)
  end
  collection
end

list = (1..5).to_a.freeze

my_each(list) do |e|
  puts "yield経由: #{e}"
end

この#my_eachでは、forループの中でyield(element)を呼んでいます。yieldは、メソッドの外から注入された一連のコード(ブロック、Proc、lambdaなど)を実行します。

なお、引数の2番目にある&blockは省略しても動きますが、ここでは「このメソッドはブロックが必須である」ということを表すスタイルとして&blockを置いています。&blockを置くと、呼び出し時にブロックが渡されないとエラーになります。なおこの記法はスタイルなので必須ではありません。

block_given?でブロックの有無をチェックしてもよいのですが、&blockを置くことで、ブロック渡し忘れエラーをメソッド実行前の引数受け取りの段階で表示でき、メソッドが中途半端に実行されずに済むというメリットもあります。

⚓ 何でも#eachでやるのは非効率

ここからが本題です。

しかしループは#eachで書いておけばよいというものではありません。#eachがRubyの重要なメソッドであるのは確かですが、具体的な処理をループの中に記述するという意味では実は#eachforと本質的に変わりません。何でも#eachでループ処理する癖を直さないと、forでやるのと同じく、C言語的な手続き型の発想に囚われたままになってしまいます。

以下は配列[1, 2, 3, 4, 5]の各要素を倍にしたものを返す簡単なコードですが、わざわざループの外でdouble = []という配列を用意して、さらにループの中でdouble << i * 2doubleに追加するところまで書くのは、他の言語ならともかく、Ruby的には非常にイケてない冗長な書き方です。

list = (1..5).to_a.freeze

double = []        # 空のdoubleをわざわざ用意している☹
list.each do |i|
  double << i * 2  # doubleの組み立てまでやっている☹
end

puts "2倍したリスト: #{double}"
#=> 2倍したリスト: [2, 4, 6, 8, 10]

⚓ #mapならこう書ける

上の例を#mapメソッドで書き直すと以下のようにずっと簡潔に書けます。#mapは結果を配列で返します。

list = (1..5).to_a.freeze

mapped = list.map do |i|
  i * 2                  # やりたいことはこれだけ😄
end

puts "2倍したリスト: #{mapped}"
#=> 2倍したリスト: [2, 4, 6, 8, 10]

参考: Enumerable#collect (Ruby 2.7.0 リファレンスマニュアル)mapcollectのエイリアスです

まず、doubleという変数を用意せずに済むというメリットがあります。

そしてもっと重要なのは、#mapの場合は「要素ごとにやって欲しい処理を外から渡す」という発想になっていることです。#mapに渡されているdoendブロックの中にあるのはi * 2だけで、処理の本質だけがずばりと記述されています。

先の#eachは「やりたいことをループの中で逐一記述する」というforと変わらない発想なので、本質的な処理以外に結果の組み立て方法まで記述しています。ある程度以上複雑な処理であればそのように記述するのもわかりますが、この処理をわざわざ#eachで書くのは車輪を再発明しているようなものです。

配列の各要素に処理を加えたものを返すという定型的な処理であれば、#map#injecteach_with_objectのようなEnumerableのコレクション系メソッドを使う方が遥かに簡潔かつ読みやすくなります。

単にこういう場合は#mapを使いましょうというだけではなく、「ループ内に処理を逐一記述する」という手続き的発想から「処理をEnumerableのメソッドに渡す」というRubyらしい発想に切り替えるところがポイントです。

⚓ 参考: #map#eachで実装して理解する

# オレオレmap
def my_map(collection, &block)
  result = []
  collection.each do |element|
    result << yield(element)
  end
  result
end

list = (1..5).to_a.freeze
my_mapped = my_map(list) do |element|
  element * 2
end
puts "my_mappedの結果: #{my_mapped}"
#=> my_mappedの結果: [2, 4, 6, 8, 10]

⚓ #inject

次は#injectです(#injectには#reduceというエイリアスメソッドもあります)。

参考: Enumerable#inject (Ruby 2.7.0 リファレンスマニュアル)

以下は配列の合計を求める簡単なコードですが、これも#eachでやろうとすると冗長かつ非効率になります。

list = (1..5).to_a.freeze

sum = 0              # 残念
list.each do |i|
  sum += i           # 残念
end

puts "eachによる合計: #{sum}"
#=> eachによる合計: 15

これも次のように#injectで簡潔に書けます。理由は#mapの場合と同じです。

list = (1..5).to_a.freeze

inject_sum = list.inject(0) do |i, j|
  i += j
end

puts "injectによる合計: #{inject_sum}"
#=> injectによる合計: 15

⚓ 補足: #sumは高速

ここでは発想の転換を説明するためにあえて#injectで書いていますが、現実に配列内の数値の合計を求めるなら、#sumメソッドを使ってlist.sumと書く方が遥かに簡潔かつ高速です。特にrangeで表された数値の合計を求める場合は#sumが断然高速です。

参考: Array#sum (Ruby 2.7.0 リファレンスマニュアル)
参考: ruby - Why is sum so much faster than inject(:+)? - Stack Overflow

ただし文字列や配列(この場合結合されます)については、#sumより#joinflattenの方が高速です。

参考: Array#join (Ruby 2.7.0 リファレンスマニュアル)
参考: Array#flatten (Ruby 2.7.0 リファレンスマニュアル)

⚓ 参考: #inject#eachで実装して理解する

# オレオレinject
def my_inject(collection, init, &block)
  folding = init
  collection.each do |element|
    folding = yield(folding, element)
  end
  folding
end

list = (1..5).to_a.freeze
my_inject_sum = my_inject(list, 0) do |i, j|
  i += j
end
puts "my_injectによる合計: #{my_inject_sum}"
#=> my_injectによる合計: 15

⚓ ハッシュもコレクションとして扱える

まずはハッシュを#eachで扱う例です。

hash = { a: 1, b: 2, c: 3 }.freeze

hash.each do |key, value|
  puts "キー #{key} の値は: #{value}"
end

Rubyには既にHash#invertというハッシュのキーと値を入れ替えたものを返すメソッドがありますが、これを#mapで再実装するとたとえば次のように書けます。

hash = { a: 1, b: 2, c: 3 }.freeze

inverse = hash.map do |key, value|
  [value, key]
end.to_h
puts "inverse: #{inverse}"

#mapが返すのはあくまで配列なので、最後に#to_hでハッシュに変換しています。

⚓ ハッシュの#injectは少々注意

ハッシュもコレクションなので、#injectで扱えます。しかし以下のサンプル(ハッシュの値を合計する)を実行するとno implicit conversion of Symbol into Integerが返ります。どこに問題があるかわかりますか?

# コケるinject
hash = { a: 1, b: 2, c: 3 }.freeze

begin
  sum_hash = hash.inject({ sum: 0 }) do |r, (key, value)|
    r[:sum] += value           # 👀
  end

  puts "sum_hash???: #{sum_hash}"
rescue => e
  puts "エラーですよ: #{e}"
end
#=> エラーですよ: no implicit conversion of Symbol into Integer

上のスライドをご覧ください。先のコードで#injectに渡したブロックの中にあるのはr[:sum] += valueになっています。#mapなら処理の結果を気にする必要がないのでラクですが、#injectは処理の結果が次の繰り返しの初期値に送り込まれるので、そこをケアする必要があります。

エラーの原因は「処理の最終行でrではなくr[:sum]が返されていたこと」です。1回目の繰り返しではr[:sum]の値は1になりますが、それが2回目の繰り返しに送り込まれると1[:sum]という無意味なハッシュになったことでエラーが発生していました。

この場合、以下のように最終行でハッシュを明示的にrで返す必要があります。

hash = { a: 1, b: 2, c: 3 }.freeze

sum_hash = hash.inject({ sum: 0 }) do |r, (key, value)|
  r[:sum] += value
  r                  # これ必要!
end
puts "sum_hash: #{sum_hash}"
#=> sum_hash: {:sum=>6}

⚓ それ、#each_with_objectでできるよ

「いちいちrを最後に置くの面倒」とお思いの方は、以下のように#each_with_objectを使えば最終行にrを置かずにスマートに書けます。

hash = { a: 1, b: 2, c: 3 }.freeze

each_with_object = hash.each_with_object({ sum: 0 }) do |(key, value), r|
  r[:sum] += value   # 今度は大丈夫😋
end
puts "each_with_object: #{each_with_object}"
#=> each_with_object: {:sum=>6}

⚓ 参考: #each_with_object#eachで実装して理解する

# オレオレeach_with_object
def my_each_with_object(collection, init, &block)
  folding = init
  collection.each do |element|
    yield(element, folding)
  end
  folding
end

hash = { a: 1, b: 2, c: 3 }.freeze
my_each_with_object_sum =
  my_each_with_object(hash, { sum: 0 }) do |(key, value), r|
    r[:sum] += value
  end
puts "my_each_with_object_sum: #{my_each_with_object_sum}"
#=> my_each_with_object_sum: {:sum=>6}

⚓ #inject#each_with_objectの違いは「副作用」にあり

先のコード例からも、#each_with_objectの挙動は#injectととても似ていることがわかりますが、違っている点もあります。それぞれの擬似コードを横に並べて見比べてみると、ほんのわずかな違いがあります。

注: 擬似コードは挙動の理解のためにこしらえたもので、実装がこのとおりかどうかについては未確認です。

両者の違いは以下の部分です。

#inject
folding = yield(folding, element)
#each_with_object
yield(element, folding)

#injectの方は、folding(この値が次の繰り返しに送り込まれる)に単にyield(folding, element)の結果を代入しています。

#each_with_objectの方は、yield(element, folding)を返しているだけで、foldingに対して操作を何も行っていません。ということは、このyieldで実行されるブロックが「foldingを改変している」、つまり渡すブロックに副作用がある場合にのみ機能するということになります。

逆に言えば、#each_with_objectに渡すブロックが副作用を伴わない場合は機能しません。以下はブロックの処理r += iで配列を改変しないので、結果は0のままです。

# 副作用なしの場合
list = (1..5).to_a.freeze

my_each_with_object_fixnum_sum = list.each_with_object(0) do |i, r|
  r += i       # 元のlistを改変していない
end

puts "my_each_with_object_fixnum_sum: #{my_each_with_object_fixnum_sum}"
# => my_each_with_object_fixnum_sum: 0

#each_with_objectならば最後にわざわざrを明示的に返さなくても動作するのは、ブロック内のr:[sum] += valueという処理がハッシュを改変しているから、というのが理由です。

⚓ おまけ1

#inject#each_with_objectには、実はもうひとつ微妙な違いがあります。

Rubyの実際の#inject#each_with_objectはどちらもブロック引数を2つ取りますが、なぜかブロック引数の順序が互いに逆になっています↓。

# inject
[1, 2, 3].inject [] do |result, i|
  result << i**i
end
#=> [1, 4, 27]

# each_with_object
[1, 2, 3].each_with_object [] do |i, result|
  result << i**i
end
#=> [1, 4, 27]

本記事の擬似コードでは順序を同じにしていますのでご注意ください。

⚓ まとめ

  • どんなときも#eachメソッドを使うのは、どんなときもfor文を使っているのと変わらない
  • コレクション系のメソッドは#eachで実装できる
  • #eachで車輪の再発明をするより、他のコレクション系メソッドでできないかを先に検討しよう

今回取り上げたメソッドをまとめると次のようになります。

メソッド 用途 戻り値
#each コレクションの各要素で処理を回す コレクション自身(変更された要素を含む)
#map コレクションの各要素を変換する 新しいコレクション(変更された要素を含む)
#inject コレクションから新しいものを作る(初期値は非破壊) 別の何か(通常は初期値の型になる)
#each_with_object コレクションから新しいものを作る(初期値を破壊) 別の何か(各要素が初期値に破壊的に作用した結果)

ツイートより

関連記事

Ruby: injectとeach_with_objectをうまく使い分ける(翻訳)

[Ruby] each_with_objectもmapも使わずにto_hだけで配列をハッシュに変換する

Ruby: Kernel#itselfの美学(翻訳)


Ruby 3: 引数をforwardする`…`記法が第2パラメータでも使えるようになった(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

読みやすさのため、原文の「argument」は訳文でメソッド定義の場合は「パラメータ」、メソッド呼び出しの場合は「引数」と表記を分けました。

Ruby 3: 引数をforwardする...記法が第2パラメータでも使えるようになった(翻訳)

Ruby 2.7で、引数をメソッドにforwardする...というショートハンド機能が追加されました。引数のforwardについて手軽におさらいしたい方は、私たちの昨年の記事「Ruby 2.7 adds shorthand syntax for arguments forwarding」をご覧ください。

⚓ 変更前

...記法は、以下のようにパラメータ全体をメソッドにforwardする書き方に限定されていました。

def travel(...)
  by_road(...)
end

しかし、以下のように第1パラメータを別にして、第2パラメータ以降を...でforwardしたい場合もあります。

def travel(preference, ...)
  if preference == "air"
    by_flight(...)
  else
    by_road(...)
  end
end

⚓ 変更後

Ruby 3.0で、第1パラメータに加えて、第2パラメータ以降を...でforwardできるようになりました(#3190)。

以下のコード例で理解してみましょう。

def transform(a, ...)
  process(a, ...)
end

def process(a, *args, **kwargs, &block)
  if block
    block.call(args, kwargs)
  else
    [a, args, kwargs]
  end
end

このコード例では、transformメソッドにaという位置パラメータと...パラメータが宣言されています。

このメソッド内で呼び出しているprocessメソッドには、transformメソッドと同じパラメータが引数として渡されています。そしてprocessメソッド定義のシグネチャには、aという位置パラメータの他に、*args, **kwargs, &blockも含まれています。

ここでのポイントは、...という記法が以下のように振る舞うことです。

  • 追加引数(複数可)が*argsに代入される
  • キーワード引数(複数可)が**kwargsに代入される
  • ブロックが&blockに代入される

transformメソッドにいくつか値を渡して、上のコードで実験してみましょう。

⚓ コード例1: 「位置引数」のみを渡す場合

  transform(1) # => [1, [], {}]

この例では、transformメソッドのaパラメータに1という値が引数として渡され、これがprocessメソッドに渡されます。

processメソッドのblockパラメータにはブロックが渡されていないので、processメソッドのelse部が実行され、[a, args, kwargs]a1という値が代入されます。

同様に、processメソッドのargsパラメータには追加の引数が渡されておらず、kwargsパラメータにもキーワード引数が渡されていません。

そしてargsのデフォルト値は[]kwargsのデフォルト値は{}です。

したがって、出力は[1, [], {}]になります。

⚓ コード例2: 「位置引数」「追加引数」を渡す場合

  transform(1, 2, 3) # => [1, [2, 3], {}]

この例のaは位置パラメータなので、位置パラメータaには1が代入され、...には2, 3が代入されます。

2, 3は追加引数と認識されるので、processメソッドの追加パラメータargsに代入されます。

したがって、出力は[1, [2,3], {}]になります。

⚓ コード例3: 「位置引数」「追加引数」「キーワード引数」を渡す場合

transform(1, 2, 3, a:1, b: 2) # => [1, [2, 3], {:a=>1, :b=>2}]

この場合、パラメータaには位置引数1が代入され、argsパラメータには追加引数[2, 3]が代入され、kwargsパラメータにはキーワード引数{:a=>1, :b=>2}が代入されます。

したがって、出力は[1, [2, 3], {:a=>1, :b=>2}]となります。

⚓ コード例4: ブロックを渡す場合

transform(1, 2, 3, a:1, b: 2) { |args, kwargs|  [args, kwargs]  }
 # =>  [[2, 3], {:a=>1, :b=>2}]

この場合、ブロックパラメータblockにブロックが渡されるので、processメソッドのif部分が実行されることになります。

processメソッドのif部分では、受け取ったブロックに追加引数とキーワード引数を渡してブロックを実行し、そのblockは渡された引数を配列にして返します。

したがって、出力は[[2, 3], {:a=>1, :b=>2}]になります。

関連記事

Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に(翻訳)

Rails: db:migrate:nameコマンドの振る舞いの変更(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: db:migrate:nameコマンドの振る舞いの変更(翻訳)

Railsアプリに以下のようなマルチデータベースの設定があり、primaryとsecondaryのデータベースがそれぞれあるとします。

default: &default
  adapter: sqlite3

development:
  primary:
    <<: *default
    database: db/development.sqlite3
    pool: 10
    timeout: 6000
  secondary:
    <<: *default
    database: db/secondary_development.sqlite3
    pool: 20
    timeout: 10000

⚓ 変更前

Rails 6でrails db:migrateを実行すると、database.ymlに存在するすべてのスキーマをダンプします。

上の場合、以下の2つのスキーマファイルが生成されます。

db/schema.rb
db/secondary_schema.rb

ここでrails db:migrate:primaryを実行したらprimaryデータベースのスキーマダンプが生成されることが期待されますが、そうならないことに気づきます。

一貫していない点がもうひとつあります。rails db:migrateを実行するとActiveRecord::Baseコネクションがオリジナルの設定に戻りますが、rails db:migrate:primaryではそうなりません。

rails db:migrateを実行した後のActiveRecord::Base.connection_db_config.inspectの結果は以下のようになります。

#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fdc09a99ee8 @env_name="development", @name="primary", @spec_name="primary", @config={:adapter=>"sqlite3", :database=>"db/development.sqlite3", :pool=>10, :timeout=>6000}, @owner_name=nil

しかしrails db:migrate:secondaryを実行した後のActiveRecord::Base.connection_db_config.inspect の結果は以下のようになります。

#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007f830df39098 @env_name="development", @name="secondary", @spec_name="secondary", @config={:adapter=>"sqlite3", :database=>"db/secondary_development.sqlite3", :pool=>20, :timeout=>10000}, @owner_name=nil>

⚓ 変更後

Railsのデータベースのダンプスキーマ(構造)が変更され(#38586)、db:migrate:nameを実行するとActiveRecord::Baseをオリジナルの設定にリセットするようになりました(#38587)。

これで、rails db:migrate:nameを実行してデータベースのスキーマファイルが生成されるようになり、これでマイグレーションを実行できるようになります。

例:

rails db:migrate:primaryを実行するとdb/schema.rbが生成されます。

また、rails db:migrate:secondaryの実行前も実行後も、ActiveRecord::Base.connection_db_config.inspectの結果が同じになります。

#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fa7d9d36ed0 @env_name="development", @name="primary", @spec_name="primary", @config={:adapter=>"sqlite3", :database=>"db/development.sqlite3", :pool=>10, :timeout=>6000}, @owner_name=nil>

週刊Railsウォッチ(20201110前編)Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

以下の公式更新情報より見繕いました。

つっつき直前に、6.1.0rc1タグができていることに気づきました↓。


つっつきボイス:「rc版が出るということはリリースはそう遠くなさそうかな」「6.1.0rc1タグはできていてzipもアップロードされているけど、releaseタブにはまだ6.1.0rc1が置かれてないんですよ」「rcはタグを付けるだけという運用なのかな?」「今まであまり気にしてなかった…」

「以前の6.0.3.rc1を見るとPre-releaseと表示されてReleaseに置かれている↑」「ということはrc版もリリースされたらReleaseタブに出るんでしょうね」「今回の6.1.0.rc1はまだPre-releaseと表示されてないから、まだ動く可能性がありそう」「rc1のアナウンスが出たら表示チェックしてみます」


その後、つっつき翌日の金曜日に6.1 RC1がリリースされました。

ちょっとわかりにくいのですが、Releasesを開いて「Show 2 newer tags」をクリックすると6.1 RC1が表示されます↓。



github.com/rails/rails/releasesより

⚓ ペンディング中のマイグレーションがある場合に画面/コンソール/ログに出力

# activerecord/lib/active_record/migration.rb#L146
    def initialize(message = nil)
-     if !message && defined?(Rails.env)
-       super("Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate RAILS_ENV=#{::Rails.env}")
-     elsif !message
-       super("Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate")
-     else
-       super
-     end
+     super(message || detailed_migration_message)
    end
+
+   private
+     def detailed_migration_message
+       message = "Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate"
+       message += " RAILS_ENV=#{::Rails.env}" if defined?(Rails.env)
+       message += "\n\n"
+
+       pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
+
+       message += "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}\n\n"
+
+       pending_migrations.each do |pending_migration|
+         message += "#{pending_migration.basename}\n"
+       end
+
+       message
+     end
  end

マイグレーションがペンディングされていたらUIやコンソールやログに表示し、どのマイグレーションがいくつペンディングされているかをすぐ把握できるようにする。
同PRより大意


つっつきボイス:「ペンディングされているマイグレーションを具体的に表示できるようになったんですね👍」「コミットやプルリクのmerge順次第では、未実行のマイグレーション番号が常に最新の番号とは限らないけど、マイグレーション番号単位で具体的に未実行のものが分かるようになるのか」

「タイトルのoutstandingって『秀逸な』という意味かと思ったら『未処理の』という意味もあるそうです」「つまりペンディング中ですね」

⚓ 新機能: paramごとにエンコーディングを設定できるようになった

従来はエンコード(アクションのすべてのパラメータをASCII_8BITとしてエンコードする)をスキップできた。
今回の変更で、アクションの任意のパラメータでparam_encodingを指定できるようになった。
この変更はGitHubでパラメータのエンコーディングを扱うときに#40124の不正なエンコーディング検出をサポートする(この変更は大きい)。
これにより、POSTのbodyパラメータがリクエストのURLパラメータと異なる可能性がある点も配慮する必要がある。
同PRより大意

# actionpack/lib/action_controller/metal.rb#L138
-   def self.binary_params_for?(action) # :nodoc:
+   def self.custom_encoding_for(action, param) # :nodoc:
      false
    end

ウォッチ20200928で取り上げた#40124と関連しているそうです。


つっつきボイス:「デフォルトのエンコーディングはASCII_8BITなのはこれまでどおりだけど、UTF-8なども指定して取り出せるようになったみたい」「Rafaelさんがコメントでハッシュ探索が1段増えるとパフォーマンスに影響するのではと指摘してますね↓」

この変更によるパフォーマンスのインパクトはどうなる?パラメータ値ごとに毎回ハッシュ探索しているが、paramsのハッシュに値が200個以上あったらハッシュ探索も200回行われることになる。この変更で、このメソッドを使わないユーザーも複雑さがO(1)からO(N)に増加したりしないだろうか?
同PRのコメント(rafaelfranca)より

「続きの#40465ではcustom_encoding_forメソッドを使わない場合に呼び出しを回避している↓」「機能を使わない場合でもパフォーマンスが落ちないよう配慮したんですね」

# actionpack/lib/action_controller/metal/parameter_encoding.rb#L18
-     def custom_encoding_for(action, param) # :nodoc:
-       @_parameter_encodings[action.to_s][param.to_s]
+     def action_encoding_template(action) # :nodoc:
+       if @_parameter_encodings.has_key?(action.to_s)
+         @_parameter_encodings[action.to_s]
+       end
      end

custom_encoding_forメソッドを自分で使うところはあまり想像できませんが、プルリクの意図はそういう感じでしょうね」

⚓ crossorigin属性を使うとリソースが2回フェッチされる問題を修正

スクリプトやCSSを(それぞれjavascript_include_tagstylesheet_link_tagで)読み込むときにcrossorigin属性が適用されると、現在のRailsでは一部のブラウザでこれらのリソースを2回読み込みが発生してしまう。理由はlinkヘッダーのpreloadディレクティブやリソース自身のcrossoriginがブラウザでリソースの再利用にマッチする必要があるため。

たとえばビューで以下のタグを使うとする。

<%= javascript_include_tag("[...snip]react.production.min.js", crossorigin: "anonymous") %>

javascript_include_tagはこのリソースをLink HTTPヘッダーにプッシュするが、現在はcrossorigin属性を無視している。
これによって上述のダブルフェッチとなる。
double fetches in network tab chrome

Chromeではwarningも表示される。

chrome warning

A preload for ‘https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js’ is found, but is not used because the request credentials mode does not match. Consider taking a look at crossorigin attribute.

このプルリクではLinkヘッダーのディレクティブに、リソース自身に渡されたものと同じcrossorigin値を含めるように変更し、これによってプリロードされたリソースをブラウザで再利用できるようにする。

double fetches fixed chrome network tab

anonymousを生成するcrossorigin: trueは既存のヘルパーで行われるので、その振る舞いをここにも複製した。
同PRより大意


つっつきボイス:「2回フェッチするってブラウザ側で起きるのか!」「そういえばcrossorigin属性というものがありますね」「javascript_include_tagで作られるHTMLの問題らしい」

参考: HTML crossorigin 属性 - HTML: HyperText Markup Language | MDN

crossorigin 属性は、 <audio>, <img>, <link>, <script>, <video> の各要素で有効であり、 CORS への対応を提供し、したがって要素が読み取るデータのために CORS リクエストの構成を有効にします。要素によっては、属性は CORS 設定属性になります。
developer.mozilla.orgより

⚓ 新機能: connecting_toメソッド

コンソールをreadonlyモードで起動されるようにするコードをGitHubで使おうと思ったが、そのためのpublic APIがなかったのでこのメソッドを追加した。
通常と異なるデフォルトコネクションが必要だが、そのコネクションをブロックで呼んでない場合がたまにある(コンソールを読み出しモードで起動するなど)。このプルリクは、アプリケーションコードで使うconnected_toの振る舞いを維持しつつ、起動時にスクリプトで特定のコネクションを設定する機能を追加する。
同PRより大意


つっつきボイス:「このconnecting_toはコントローラーやモデルロジック内で使うことは想定していなさそうです」「たとえばRailsコンソールのようなインタラクティブ端末内で以後の入力コードのデフォルト接続先を変えたいときなどに、このconnecting_toでできるということらしい」「へ〜」「Railsコンソールでconnecting_toを使って接続先を変えれば、readonlyな状態で操作したいなどのケースではコンソール操作でconnected_toブロックを書き忘れてもconnecting_toで設定したコネクションが使われるので、そのような用途に便利なんでしょうね」「なるほど」

# activerecord/lib/active_record/connection_handling.rb#L175
    # 特定のコネクションを使う。
    #
    # このメソッドは特定のコネクションが使われるようにするときに有用。
    # (コンソールをreadonlyモードで起動するときなど)
    #
    # このメソッドは`connected_to`と違ってブロックでyieldしないので、requestで用いることは推奨されない。
    def connecting_to(role: default_role, shard: default_shard, prevent_writes: false)
      if legacy_connection_handling
        raise NotImplementedError, "`connecting_to` is not available with `legacy_connection_handling`."
      end

      prevent_writes = true if role == reading_role

      self.connected_to_stack << { role: role, shard: shard, prevent_writes: prevent_writes, klass: self }
    end

connected_toはブロックの中でしかコネクションが有効にならないんですけど、Railsコンソールのように自由に操作できる環境だとconnected_toのブロックを律儀にreadonlyを付けて書くとは限らないので、上のコード例のように書くことでコネクションに縛りをかけた状態で操作できるということだと思います」「readonlyモードを使いたくてこのメソッドを作ったのかもしれないと想像してみました」

⚓ データベースタスク作成でyamlを読めない場合に警告を出すようになった


つっつきボイス:「database.ymlの警告を何度も出すのではなく最初に1回だけ出すようにしたらしい」「for_each(databases)に変わったところからするとマルチプルデータベース関連の修正っぽいですね」「たしかにdatabasesが複数形になってる」

# activerecord/lib/active_record/railties/databases.rake#L28
-   ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+   ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
      desc "Create #{spec_name} database for current environment"
      task spec_name => :load_config do
        db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
        ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
      end
    end
  end

「こちらのメッセージもマルチプルデータベース向きになってる↓」「database.ymlが書式レベルでは正しくても内容が正しくないような場合に、従来は最初のステップがパスして次のステップでうまくいかなかったのを、最初のステップで警告が出るようにチェックを加えたんでしょうね」

# activerecord/lib/active_record/tasks/database_tasks.rb#L144
-     def for_each
+     def setup_initial_database_yaml
        return {} unless defined?(Rails)

-       databases = Rails.application.config.load_database_yaml
+       begin
+         Rails.application.config.load_database_yaml
+       rescue
+         $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB."
+
+         {}
+       end
+     end

「テストを見ると、ERBの中身が入っていないような場合にdb_create_with_warningしてる↓」「余分なエラーが減るのは嬉しいですね」

# railties/test/application/rake/dbs_test.rb
      test "db:create and db:drop show warning but doesn't raise errors when loading YAML with alias ERB" do
        app_file "config/database.yml", <<-YAML
          sqlite: &sqlite
            adapter: sqlite3
            database: db/development.sqlite3
          development:
            <<: *<%= ENV["DB"] || "sqlite" %>
        YAML

        app_file "config/environments/development.rb", <<-RUBY
          Rails.application.configure do
            config.database = "db/development.sqlite3"
          end
        RUBY

        db_create_with_warning("db/development.sqlite3")
      end


最初のdatabase.ymlを一旦読み込み、タスクを作成できない場合はwarningを出す。

マルチプルデータベースではRailsアプリケーションの起動前にdatabase.ymlを読み込んでタスク生成を試みる。これはERBをストリップする必要があるが、これはRailsコンフィグを読み出している可能性があるため。

#36540のようないくつかのケースではERBが複雑すぎて#35497で自分たちが使ったDummyCompilierでは上書きできない。複雑さが原因のときは、database.ymlからデータベースタスクを推測できないというwarningを単に出力している。

自分はこの作業をやりながら、同じwarningが何度も出力されるのを避けられるよう、database.ymlを最初に1度だけ読み込むようにコードを更新することに決めた。なお自分のテストではパフォーマンスへの影響はなく、単にエラーをどこかに保存せずに済むようにしたかった。それにこの方がクリーンに思える。

なおこの変更で既存の実行中タスクは壊れない(単にdb:create:other_dbのようなマルチDB向けタスクが生成されなくなる)。database.ymlが実際に読み取り不可能な場合は、通常のrakeタスク呼び出し中に落ちる。
修正対象: #36540
同PRより大意

⚓ TimeWithzoneDateTimeの比較を修正

問題点
#40413TimeWithZoneDateTimeと比較したときに丸めの問題が生じた(失敗するテストを書いたにもかかわらずパスした)。
修正方法
この問題はtime_instance.to_fを用いたときに発生した(特定のケースで精度が不足することがあった)。Timeインスタンスの作成をtime_instance.to_rでRationalにすることでこの問題の解決を試みた。これによってパフォーマンスがわずかに落ちるが引き換えに精度を得られる。
同PRより大意


つっつきボイス:「プルリクで参照している#40413↓の見出しにあるけど、Time.zone.atをTimeWithZoneで呼び出すときとTimeで呼び出すときで振る舞いが異なる場合があったとは」

「TimeWithZoneの場合はto_rすることで修正している↓」「Rationalに変換してるんですね」「DateTimeの場合はこれまでどおりto_fすべきなのか」「この問題よく見つけたな〜」

# #L
    def at_with_coercion(*args)
      return at_without_coercion(*args) if args.size != 1
      # Time.at can be called with a time or numerical value
      time_or_number = args.first

-     if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
+     if time_or_number.is_a?(ActiveSupport::TimeWithZone)
+       at_without_coercion(time_or_number.to_r).getlocal
+     elsif time_or_number.is_a?(DateTime)
        at_without_coercion(time_or_number.to_f).getlocal
      else
        at_without_coercion(time_or_number)
      end
    end
    alias_method :at_without_coercion, :at
    alias_method :at, :at_with_coercion

⚓Rails

⚓ 🌟Railsアプリに最適なEC2インスタンスタイプとは(Ruby Weeklyより)🌟


つっつきボイス:「TechRachoの翻訳記事↓でもお世話になっている、ベンチマークに強いNoah Gibbsさんの記事です」「Railsに最適なEC2インスタンス!」「そこそこ長い記事ですね」「駆け足で読んでみますか」

Ruby 2.5.0はどれだけ高速化したか(翻訳)

「burstableなT4インスタンスは短期間のベンチマークだと速いけど長期間だと速度が落ちると書かれてる」「たしかにT系インスタンスはバーストパフォーマンスインスタンスのCPUクレジット問題があるので長期間のベンチマークのようなものに使うべきではないでしょうね: 長期実行だとCPUクレジットを使い切ってしまうという基本的な話」「なるほど」

参考: バーストパフォーマンスインスタンスの CPU クレジットとベースライン使用率 - Amazon Elastic Compute Cloud

「C5などのC系はCPUを重視するコンピューティング最適化インスタンスで、こちらはバーストクレジットがないので動かし続けられます」「RubyにRactorが正式に入ったらRailsでもC系インスタンスを使えるかもしれないと書かれてる」

参考: Amazon EC2 C5 インスタンス | AWS

「記事ではDiscourseのRailsアプリ↓が現実に近いということでベンチマークに使っている」「Railsのプロセスあたりのスレッド数のような具体的な数値が書かれてますね」「Discourseアプリの場合、EC2の2xlargeインスタンスだと1プロセスあたり6スレッドにしたときにプロセス10個でvCPUやメモリをだいたい使い切る感じらしい」「いつだったかRubyKaigiでこのあたりの話を聞いたような気がする🤔

discourse/discourse - GitHub

「記事ではAWSのARMインスタンスやdedicated instance(ハードウェア専有インスタンス)にも触れてますね」「やったことないけどARMでCRubyって動くのかな?」「CRubyのコンパイルぐらいまではできるかもしれないですね」

参考: Amazon EC2 A1 インスタンス | AWS
参考: Amazon EC2 ハードウェア専有インスタンス | AWS

「T系やC系やM6gインスタンスは、特殊なケースならともかく、一般にはRails向けに万人にはおすすめしない、特にスピードテストでは結果が正しくなくなると書かれてる」「なるほど!」

「記事の途中で『M4かM5のどちらか』と書かれてる」「たぶんRailsのメモリ使用量だとそのぐらいが適切で、かつCPUバーストクレジットもないから、特にこだわりがなければM4かM5に絞られるということなんでしょうね」「記事の終わりの方を見ると、コスパも見ると実はオンデマンドインスタンスならM4よりM5の方が安いみたいなことが書かれてる」「なおオンデマンドインスタンスは、スポットインスタンスと違ってコストが固定されます」

参考: Amazon EC2 スポットインスタンス | AWS
参考: オンデマンドインスタンスの料金 - Amazon EC2 (仮想サーバー) | AWS

「インスタンスタイプの一覧↓を見ると『コンピューティング最適化』とか『ストレージ最適化』とかいろんなカテゴリがあるんですね」「M4やM5というとメモリが多めの汎用インスタンスという印象でしたけど、最近だと『メモリ最適化』に含まれるR系がそれ用なのかな」

参考: インスタンスタイプ - Amazon EC2 | AWS

「急いで読んだ範囲では、思った以上に経験に裏打ちされた非常に具体的な記事で、読んでてとても納得がいきます👍」「AWS事情もみっちり解説されてて、親切丁寧さがスゴい」「もっと概念的な話かと思ったら具体的ですね」「Noah Gibbsさんらしい説得力のある記事」

「AWSのEC2でRailsを運用したことがない、または経験の少ないRailsエンジニアはこの記事を読んでおくといいんじゃないかと思います: おそらくそういう人にとって知らない用語がいっぱい出てくると思うので調べながら読むことになるでしょうけど」「この記事翻訳してもらっていいですか?」「はい、この後でオファー出します」「これはいい記事❤


超久しぶりに🌟を進呈します。おめでとうございます。

その後Noah GibbsさんからOKをいただきましたので、近々同記事の翻訳を公開します。ご期待ください。

⚓ Dockerコンテナ内のSSHコンフィグをWSL2で使い回す(Ruby Weeklyより)


つっつきボイス:「Dockerコンテナ内のSSHコンフィグをWSL2で使い回すということは、$HOME/.sshディレクトリをボリュームでマウントしようねという話かなと思ったら本当にそうだった↓」「予想通りでした😆

# 同記事より
    volumes: 
      - type: bind
        source: ${HOME}${USERPROFILE}/.ssh
        target: /home/${DEV_USER:-abapa}/.ssh

⚓ rubocop-rails_config: Rails公式のRuboCop設定と同じスタイルを使える(Ruby Weeklyより)

toshimaru/rubocop-rails_config - GitHub


つっつきボイス:「officialという言葉があったのでRails公式のRuboCopコンフィグかなと思ったら、Rails公式のRuboCopコンフィグを持ってきてそれと同じスタイルにできるというgemだそうです」「inherit_gemで指定できるのね↓」

# 同リポジトリより
inherit_gem:
  rubocop-rails_config:
    - config/rails.yml

「自分で公式のコンフィグをコピペすれば同じことができますけど、gemでインストールする意義って何だろう?🤔」「gemでやれば公式のスタイルが更新されたときに追従できそうですね」「たしかに」「おそらくRails公式のRuboCopコンフィグは今後もRuboCopのバージョンアップにも追従するでしょうから、RuboCopをバージョンアップしやすくなるかもしれない」「Railsのスタイルに常に合わせておきたい人によさそう」

⚓ n_plus_one_control: N+1クエリをマッチャーで調査するgem(Ruby Weeklyより)

palkan/n_plus_one_control - GitHub

RSpecとminitestのどちらでも使えるそうです。


つっつきボイス:「Rails技術記事↓でお馴染みのEvil Martiansがスポンサーになっている、テストのマッチャーでN+1クエリを検出するツールの記事です」

成熟したRailsアプリのフロントエンドを最新にリニューアルする方法(翻訳)

N+1クエリ問題が本当に起きているかどうかを確認するには最終的にコードを動かさないといけないんですけど、このgemはこんなふうに↓『N=2のときは5回』『N=5のときは8回』というふうにテストで実際に回してヒントを出してくれるgemみたいですね」「あ〜なるほど」


同リポジトリより

「↓以下のような感じでスケールファクターを渡して実際にテストを回してみると、N+1クエリ問題が起きていればNの増加に応じて回数が指数関数的に増大するはず: 上のスクショではNと回数の差が常に3なのでN+1は起きていませんが」

# 同リポジトリより
# You can specify the RegExp to filter queries.
# By default, it only considers SELECT queries.
expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)

# You can also provide custom scale factors
expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)

「N+1クエリ問題の検出によく使われるbullet gem↓はコードをチェックすることで自動検出しますけど、このn_plus_one_controlは実際にいくつかのスケールでクエリを投げていろんな実行回数を出して、それを人間が見てチェックするということでしょうね」「なるほど」

flyerhzm/bullet - GitHub

「n_plus_one_controlは入れただけで自動的にN+1クエリ問題を検出してくれるものではなく、N+1クエリ問題が起きている疑いがある場合に自分で当たりをつけて発生箇所を探すためのツールだと思います👍


Rails: Bulletで検出されないN+1クエリを解消する

⚓ Meilisearch: Rust製の全文検索エンジン

meilisearch/MeiliSearch - GitHub


つっつきボイス:「Rust製のMeilisearchという全文検索エンジンをRailsで使ってみたという記事です」「外部サービスではなくてMeiliSearchサーバーを自分で動かしてやるものみたい」「手元の辞書を見ると、Meiliは人の名前っぽい雰囲気でした」

「タイポや同義語もよしなに解釈してくれるらしい↓」「漢字もサポートしてる!」「漢字はサポートされてても日本語の文法までサポートされているかどうかはわかりませんけど」「あ、たしかに」「軽くググった限りではMeiliSearchの日本語記事が見当たらないので、日本語はこれからなのかもしれませんね」「他のエンジンのtokenizerも使い回したりできたら嬉しいかも」

  • 機能
    • インクリメント検索(50 msec以内の応答)
    • 全文検索
    • タイポ許容(タイポやスペルミスを解釈する)
    • さまざまな検索・フィルタオプション
    • 漢字のサポート
    • 同義語のサポート
    • インストール・デプロイ・メンテが容易
    • 全文を返す
    • 高度なカスタマイズ機能
    • RESTful API

「全文検索機能はあまり使ったことがないけど、Meilisearchは割とホットなプロジェクトみたい」「★が9900個もあってリポジトリも活発みたいです」「こういうものがあることを知っておくとよいでしょうね」


後でMeiliSearchの公式サイトも見つけました↓。公式ツイートはつっつき後に見つけたものです。


追いかけボイス: 「MeiliSearchの同義語サポート↓、one-wayだけじゃなくmulti-wayに同義語検索するようにもできるのがなかなか面白い: 頑張ってDB作るとかなり柔軟&有用な検索が作れそう」

参考: Synonyms | MeiliSearch Documentation v0.16


前編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201028後編)RuboCop 1.0.0 stable版がリリース、Ruby DSLのGUIフレームワークGlimmer、Keycloakほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20201111後編)RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓Ruby

⚓ RubyConf 2020が11/17〜11/19にオンライン開催


つっつきボイス:「今年のRubyConf 2020はオンライン開催かつチケット制なのか」「チケットは有料なんですね」「今回のスケジュールは3日間の開催で、スロットは最大で3つになってます」「英語が基本になりますけどオンラインなら多少追いやすいかも」

⚓ RubyConf 2020のセッション

スケジュールの時間帯はCentral Time(CST: 米国中部標準時)だそうです。

スケジュールを見ると興味深そうなセッションがいろいろ並んでいますね。タグで色分けされているのでつっつき後に少し追ってみました。通常のカンファレンスに近づけるためにいろいろ工夫をこらしているように思えました。

  • RubyConf 2020タグ(紫)は全体企画、Keynote(黄)はキーノート。


rubyconf.orgより

  • Talkタグ(赤)は通常のセッション。


rubyconf.orgより

  • Sponsorsタグ(緑)を見ると、詳しくはわかりませんが、Q&AをSlackでやりとりするスポンサーセッションもいくつかあるようです。


rubyconf.orgより

  • スポンサー企業と話ができる「Sponsor Job Fair」という時間も設けられていて、従来のスポンサーブースに相当するようです。


rubyconf.orgより


  1. Does RubyConf have a jobs fair or list?
    Yes, we will be conducting a virtual job fair on the second day of RubyConf 2020 and will also be featuring an official jobs list.
    FAQより
  • Hallway(青)タグのセッションは、おそらく会場の廊下(hallway)に集まってコードを書いたり立ち話や会食をしたりという雰囲気の再現を目指しているように思いました。


rubyconf.orgより

  • Hallwayとは別に1日目と3日目にWorkshop(灰)タグのセッションもあり、その時間帯だけスロットが最大で3つになっています。



rubyconf.orgより


以下はつっつき後に見つけたツイートです。

⚓ 「RubyにSTM(Software Transaction Memory)を追加する提案」を吟味する(Ruby Weeklyより)


つっつきボイス:「Rubyにソフトウェアトランザクショナルメモリを入れようという話があるとは知りませんでした」「STMがわからない😅」「RubyのSTMはどこかで見かけたような気がする(注: 後述の笹田さん資料を参照)」「詳しい人に教わりたいです」

参考: ソフトウェアトランザクショナルメモリ - Wikipedia

計算機科学において、ソフトウェアトランザクショナルメモリ(英: software transactional memory, STM)は、データベーストランザクションに似た並行性制御機構であり、並列計算を行う際の共有メモリへのアクセス法である。この機構はロックベースの同期を用いた並行性制御の代替手段として機能し、ノンブロッキングな方法で実装される物もある。ここでいうトランザクションとは、共有メモリに対する一連の読み出しと書き込みを実行するコードを意味する。論理的にはこれらの読み出しと書き込みは、時間的なある一点で行われ、他のトランザクションからはその間の状態は見えない。
Wikipediaより

「記事では回路を生成するコードでみっちりデモをやってる↓」「STMとデモプログラムの結びつきが今ひとつよく分からなかったので、時間かけて記事読まないといけなさそう」

chrisseaton/ruby-stm-lee-demo - GitHub

生成にはリーのアルゴリズムを使っているそうです。


同リポジトリより


後で調べると、@_ko1さんによる2018年の「第120回プログラミング研究発表会(SWoPP2018)」発表資料PDFにSTMについて言及がありました。

アクセス時にトランザクション制御を用いる、いわゆる、ソフトウェアトランザクショナルメモリ(STM)を用いるオブジェクトについても検討している。
(中略)
例えば、共有データが複数あるとき、データをまたがって一貫性が求められるような場合、トランザクションを適切に制御することが必要になるが、それを誤らずに制御可能に誘導するインターフェースが重要である。すべての共有コンテナオブジェクトを STM にすることで、複数の共有データにまたがったトランザクションを、ロックの順番によるデッドロックの心配なく実現することができるので、STM は有力な手法である。しかし、一貫しなければならない処理に対して、複数トランザクションを実行してしまうようなプログラムミスを検出できない。
同PDF p4(笹田)より

⚓ LibSassが非推奨に

なおruby-sassは既に非推奨となってアーカイブされています。

sass/ruby-sass - GitHub


つっつきボイス:「C++で書かれたLibSassが非推奨になったそうです」「SassはDart製のDart Sass↓に統合されるという話が前からあって移行パスも用意されていましたけど、ついにLibSassも正式に非推奨になったのか」「あ、前から決まってたんですね」

参考: sass/dart-sass - GitHub

参考: Dart Sass、使ってる?Preprosを使えばコンパイルも楽勝! | Webクリエイターボックス


以下のsass/sassc-rubyはLibSassを使っているので影響を受けることになりますね。

sass/sassc-ruby - GitHub

⚓ その他Ruby

つっつきボイス:「Ruby biz Grand prixといえば、BPSも『入退くん』という入退出管理Webサービスをエントリーしたことありますよ↓」

参考: [2]Ruby biz Grand prixで受賞した企業の声:さまざまなビジネスに拡がるRubyの活用事例から大賞が決定!『Ruby biz Grand prix 2018』|gihyo.jp … 技術評論社


gihyo.jpより

入退くんをはじめとする自社サービス「くんシリーズ」の運用方針を紹介します。


つっつきボイス:「たしかにReactorとThreadはどちらもスレッドの制御を行うので、一方だけを使うならともかく両方を組み合わせて使うのは大変そうですね」

⚓DB

⚓ Percona Toolkitとpt-online-schema-changeの危険なエッジケース


つっつきボイス:「前回取り上げようと思って忘れてた記事です」「CREATE TABLE文とかを書き換えたりするツールだから、危険なエッジケースがあったというのもわかる気がする」「これはバグですね」「たしかにPercona Toolkitの3.0.10以下が該当する問題って書かれてる」

「記事によるとpt-online-schema-changeの基本動作は、内部でtest_not_nullテーブルをtest_nullに変えるときに、まず_test_not_null_newというアンダースコア付きのテーブルを作り、次にINSERT LOW_PRIORITYを使って既存テーブルからデータを入れている」「ふむふむ」「pt_osc_test_test_null_delのようにDELETEとUPDATEとINSERTのトリガーも作るので、INSERT LOW_PRIORITYの実行中に整合性を保てるようになってる、そして最後にトリガーや_test_not_null_newテーブルをDROPする↓」

# 同記事より
$ ./pt-online-schema-change-3.0.10 u=msandbox,p=msandbox,h=localhost,S=/tmp/mysql_sandbox5735.sock,D=test,t=test_not_null --print --alter "engine=InnoDB" --execute
(...)
Altering `test`.`test_not_null`...
Creating new table...
CREATE TABLE `test`.`_test_not_null_new` (
`id` int(11) NOT NULL,
`add_id` int(11) NOT NULL COMMENT 'my generated test case',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Created new table test._test_not_null_new OK.
Altering new table...
ALTER TABLE `test`.`_test_not_null_new` engine=InnoDB
Altered `test`.`_test_not_null_new` OK.
2020-09-30T21:25:22 Creating triggers...
2020-09-30T21:25:22 Created triggers OK.
2020-09-30T21:25:22 Copying approximately 3 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_not_null_new` (`id`) SELECT `id` FROM `test`.`test_not_null` LOCK IN SHARE MODE /*pt-online-schema-change 1438 copy table*/
2020-09-30T21:25:22 Dropping triggers...
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_ins`
2020-09-30T21:25:22 Dropped triggers OK.
2020-09-30T21:25:22 Dropping new table...
DROP TABLE IF EXISTS `test`.`_test_not_null_new`;
2020-09-30T21:25:22 Dropped new table OK.
`test`.`test_not_null` was not altered.
2020-09-30T21:25:22 Error copying rows from `test`.`test_not_null` to `test`.`_test_not_null_new`: 2020-09-30T21:25:22 <strong>Copying rows caused a MySQL error 1364:</strong>
Level: Warning
Code: 1364
Message: Field 'add_id' doesn't have a default value
Query: INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_not_null_new` (`id`) SELECT `id` FROM `test`.`test_not_null` LOCK IN SHARE MODE /*pt-online-schema-change 1438 copy table*/

「でもDEFAULT NULL COMMENTを指定したカラムがあると移行後にそのカラムがNULLになってしまったのか」「このバグを知らずに踏んだらキツい」「実行後の確認は欠かせませんね」「バグが直ってよかった😂

# 同記事より
mysql [localhost:5735] {msandbox} (test) > select * from test_null;
+----+--------+
| id | add_id |
+----+--------+
|  1 |   NULL |
|  2 |   NULL |
|  3 |   NULL |
+----+--------+
3 rows in set (0.00 sec)

⚓リモートワーク

⚓ ZoomがEnd-to-End暗号化機能のtechnical preview版をリリース


つっつきボイス:「ついにZoomがEnd-to-End暗号化(E2EE)をできるようになる」「E2EEだとクラウド録画ができなくなって手元でしか録画できなくなるなどの制約が付きそうですけど」「Zoomサーバー側で生成する暗号化鍵ではなくて、ミーティング参加者の生成した鍵でE2EEするんですね」「Zoomが生成した(Zoomが知り得る)鍵を使いたくないユーザーにとっては嬉しいのかも」

「ミーティング参加者の鍵はどうやって配布するんだろう?」「誰かが作った鍵を公開鍵暗号とかを使って共有するのかな?」

⚓ ビデオ会議でE2EEが要求されそうな用途

「ところでビデオ会議がE2EEかどうかって気にしてますか?」「自分が気にするかどうかというよりは、ビデオ会議でやりとりする顧客側が気にするかどうか次第でしょうね」「そうなんですね」「極端に言えば、クライアントにマルウェアが入ってしまったらE2EEがあっても同じだと思います」「あ、たしかに」「Zoomは現在でも通信路は暗号化されていますし、E2EEにするかどうかは最終的にZoomを信頼するかどうかという話だと思っています」「なるほど」

「たぶん今度の機能追加で、Zoomに全面的な信頼をおくわけにいかない事情のあるユーザーが、E2EEがあることで自分の組織にZoomの利用を認めてもらいやすくなるというメリットはあるでしょうね: たとえば米国の検閲を受ける可能性のある会社とか、国や軍事関連のような特殊な業種のユーザー」「そういう業界はあまりZoomを使わなさそうな気もしますけど、E2EEがあれば急にZoomを使う必要が生じたときでも組織から利用許可が出やすそうですね」「もちろん一般ユーザーにとっても、よりセキュアな方法でビデオ会議ができる選択肢が増えるのはいいことだと思います👍

⚓クラウド/コンテナ/インフラ/Serverless

⚓ joker1007さんの「1000万件オーバーのレコードのデータをカジュアルに扱うための心構え」


つっつきボイス:「はてブで大バズりしていた記事です」「joker1007さんのこの記事はもうおっしゃるとおりだと思います👍」「社内の現場向け教育資料を元にしているというのもよいですね」「現場で得た具体的な知見のエッセンスがまとめられているのが嬉しい」

「『既存のコードを信用するな』『手癖で書くな』は、まさにそのとおり」「ちゃんと理解して把握してから構築を開始しようねという基本的な話ですね」「自分でコードを書く部分が増えるほどそこがバグの温床になる可能性も増えるので、可能ならなるべくクラウドにある機能を活用すべきだと思います」

⚓ Docker Hub関連記事(続報あり)


つっつきボイス:「AWSから50GBまで無料でプルできるのはいい👍」「外からプルできるんですね」「BPSのGitLabもAWSに乗っているんですけど、GitLab CIでDocker Hubからの転送容量制限に引っかかったときにこれでやれたらありがたい」

「どちらかというとAWSのコンテナレジストリよりは、Docker Hubからのプルをミラーしてくれるサービスの方が欲しいかな: コンテナレジストリが増えてあちこちに分散するのは望ましくないので、いわゆるディストリビューションのパッケージリポジトリでやっているような感じでコンテナレジストリのミラーサーバーがある方が嬉しいかも」「たしかに」

「同じAWS記事に、Docker Hubが6か月以上使われなかったコンテナイメージを削除するという方針が保留になったとありますね」「使うものはDocker Hubからプルしてるでしょうから、使わないのは削除でもいいのかとちょっと思いましたけど」「でもCIでキャッシュに乗っていたらプルされませんよ」「あ、そうか」「改めてプルするまでは手元のキャッシュにあるものが使われます」「それである日突然コンテナイメージが消えたら困っちゃいますね」「しかもコンテナイメージが消えたときにはわからなくて、サーバーを新たに再構築したときなどに初めて気づくヤツでしょうね😆」「それは惨事…」


「もうひとつの記事はDocker Hubの無料プラン!」「承認されたオープンソースの名前空間からイメージを取得する場合は制限なしということか」「オープンソースプロジェクトとして認められるための条件が付いてますね↓」

  • パブリックかつ非商用であること
  • Open Source Initiative (OSI)の定義(無償配布、ソースコード、派生物、ソースコードの統合、差別を許容しないことなどの定義を含む)を満たすこと
  • OSIが承認するオープンソースライセンスのもとでイメージを配布すること
  • アプリケーションの実行に使われるDockerイメージを作成すること

「多くのコンテナイメージはUbuntuやAlpineなどのオープンソースOSをベースにしていますけど、ビルドでそれらをプルするときには制限がなくなるということなんでしょうね」「オープンソースの作者はDocker Hubでこれ使っていいよと連絡しないといけないということですか?」「そういう申請は必要そうですね」「もしオープンソースソフトウェアの作者が申請しないままだとリジェクトされるということに…?」

参考: Expanded Support for Open Source Software Projects - Docker Blog

元記事で参照している上の英語記事によると、オープンソースコミュニティ向けの以下のフォームで申請が必要になるそうです。

参考: Open Source Community Application | Docker

⚓ 続報: GitHub Container Registryでやれる

以下のツイートはつっつき後に見つけました。

⚓JavaScript

⚓ yarn whyコマンド


つっつきボイス:「yarn whyなんてコマンドができたのか」「そういえば少し前からGitHubにセキュリティアラートができましたね↓」「そうそう、久々にGitHubにログインしたら使えるようになったのでオンにしてみました」

参考: Security | GitHub

「セキュリティアラートへの対応は基本的にソフトウェアをアップデートすることになりますね」「元記事によると、自分のpackage.jsonに書かれていないけど間接的に依存しているパッケージがアップデート対象の場合にもGitHubセキュリティアラートがチェックしてくれるそうです」「yarn whyでそういう間接的な依存パッケージを調べられるということですね」「Rubyのbundlerにも似たような機能があった↓」

[Ruby] Bundler 1.15の全コマンド

「なおyarn whyは以下の記事で知りました」「ここが定期的に更新されるようになってうれしいです😋

参考: 週刊気になったITニュース(2020/10/25号) - masa寿司の日記

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓ HTML5のblock-levelとinline-level


つっつきボイス:「はい、HTML5ではブロックレベル要素とインライン要素という概念はなくなっています」「しまった、自分まだHTML4脳だったか😅

「HTMLやCSSの仕様については弊社のbabaさんが詳しいんですけど、以前はブロックレベル要素とインライン要素があったのが、MDNにもあるようにHTML5ではそれらが定義されなくなって↓displayプロパティでいくらでも変更できるようになっています(CSSには引き続き同じような概念はありますが)」「あ〜なるほど」「HTML5の世界からはそういう概念が外されたんですね」

参考: インライン要素 - HTML: HyperText Markup Language | MDN

以下の要素は既定でインラインです (ただし、ブロック要素とインライン要素は HTML5 では定義されなくなり、代わりにコンテンツカテゴリが使用されます)。
developer.mozilla.orgより

「HTML4以前だと、たとえばインライン要素の中にはブロック要素が書けないといった制約がありましたけど、そういうのがなくなったということだと思います」「HTML5脳にならなきゃ😅

「ただ、概念はなくなりましたけど、現場レベルでは今もそういう概念がまだあるかのようにHTMLを書いていることは多いでしょうね」「手癖で今までどおり書いちゃうのはありそう」「自分も意識せずにそう書いているかも」「ツイートにもあるように、HTML5では便宜的にそういうものが残っていて実務上もあまり問題ないと理解しておけばよいと思います」「それに<div>なのにdisplay: inlineとかになってたら気持ち悪いですよね」「あ〜それ無理です😆

「現場レベルのHTML5の書き方は基本的に変わらないと思いますが、今はHTMLを手書きすることが減って、さまざまなフロントエンドフレームワークで自動生成することが増えてきているので、HTML4のような制約があると何かの拍子で自動生成HTMLが仕様に違反してしまう可能性があるかもしれませんね: その意味ではHTML5のように柔軟に定義できる方がフレームワーク側でHTMLを生成しやすいかもしれないと思いました」「そうかも」

「考えてみればブロック要素はインライン要素はデザインやレイアウトに関連するものだから、それもあってHTML5から切り出したのかもしれない」「たしかに文書構造ではありませんね」「より純粋なマークアップ言語を目指したのかも」

⚓ その他フロントエンド


つっつきボイス:「いらすとや以外の選択肢としてよさそうかなと思って拾いました」「たしかに、いらすとやの絵はあまりに普及していますよね」「このサイトのイラスト、色をブラウザ上で変更できますよ!」

「ところで利用規約を見ると『商用・非商用問わず自由にwebサイトや印刷物等に利用頂けます』『利用規約範囲内のイラストに対する編集や加工は可能ですがそれに伴う著作権の譲渡や移動はありません』などとなってるのか🤔」「加工して使うのはOKそう」

「この利用規約については、既に確立しているライセンスが使われていたらなおよかったかなという気持ちがあります」「あ、クリエイティブ・コモンズのような既存のライセンスですね」「たとえば『その他著作権を侵害する行為は禁止です』の『その他著作権』の部分に解釈の余地が考えられますし、利用規約が将来変更されて使えなくなったりする可能性などについても一応使う前に考慮が必要でしょうね」

参考: クリエイティブ・コモンズ・ジャパン

⚓言語/ツール/OS/CPU

⚓ ExcelでVBAを使わずにドラクエを作る


つっつきボイス:「これ見た見た」「これはスゴい」「VBAなしでよくぞここまで」「しかも1か月以上もかけて」「Excelでどえらく頑張ってる」「F9キーだったかな、一箇所どうにもできない部分があったのも面白かった😋」「こういう縛りプレイ的なコーディングを愛する分野ってありますよね」

「元記事に『最後まで見た方が面白いですよ』ってあったので動画の最後を見たらあのイルカが登場してました🐬」「あのイルカ」「あのイルカだ」「あのイルカ、見かける機会が既になくなりましたよね」「若い世代だとほとんど知らないんじゃないかしら」「インターネットミームの一種か何かと思われてたりして😆

後でイルカの名前をこちらで見つけました↓。

参考: 正式名称アーカイブス|ワードやエクセルのイルカの名前

参考: インターネット・ミーム - Wikipedia

⚓その他

⚓ スマートロック


つっつきボイス:「TechRachoにも記事を書いていただいているWingdoorさん↓のメンバーが執筆中の記事でスマートロックというものを知りました」「スマートロックは何年も前から使われてますね」

詳しくは今後公開するWingdoorさんの記事でどうぞ。

「スマートロックは鍵を時間制限付きで渡すこともできたりするので、民泊と相性がいいですよね」「民泊なら何かあったら持ち主が見に行けばいいや、みたいな感じで使えそう」「でも自宅をスマートロックにしたらトラブったときに家に入れなくなったりするのかな?」「たいていのスマートロックは既存の鍵にかぶせる形で取り付けるから、普通の鍵でも開けられますよ」「なら大丈夫そうですね」「つまり物理鍵は結局持ち歩かないといけなくなる?」「そうなりますね」

参考: 民泊 - Wikipedia

「ちなみにスマートロックって、マンションの管理組合によっては認められないこともあるんですよ(エントランスロック式のマンションなどでは、入居者の誰かひとりのスマホが脆弱な管理になるだけで侵入される問題も考えられるので)」「あ、たしかに」

「ちょうど見つけたツイートでもこんな事例がありました↓」「やっぱり物理鍵要るのかな…」「スマートロックデバイスの電池が切れる可能性も考えられますね」

参考: SwitchBot(スイッチボット)カーテン | 太陽の光で朝スッキリ!ワンタッチで自動化&楽々操作

「私はスマート何とかみたいなデバイスは大好きで、カーテンを自動で開閉するデバイス↑とかなら自宅に入れてますし楽しいですけど、自宅の鍵をスマートロックにするのはまだためらいがありますね」「自宅をスマートロックにすると、たとえば鍵を家の中に置いたまま家から閉め出されたときに代替手段がないのが心配」「ホテルや自動車でよくあるインキー(インロック)ですね」「そうなったらもう鍵屋さんを呼ぶしかない🗝」「あれを一度でも経験すると身に沁みます」

⚓ スマートロックはオフィスで便利

「ところで、スマートロックをオフィスで使うのはありかなと思います」「あ、それいいですね!」「たとえば仕事が終わって帰ろうとしたら物理鍵を誰かが持って帰ってしまって戸締まりできないときとか、朝イチで出社したらまだ鍵を持ってる人が出社していなくてオフィスに入れないときでも、管理権限持っている人に連絡して鍵をスマホに送ってもらって開ける、といったことができるのがいいですよね😋」「たしかに!」

「そもそも物理鍵を他人に渡すのって多少なりともリスクがあるじゃないですか」「合鍵を作られる可能性があるからですね😆」「スマートロックなら鍵を時間限定で送信できるので、物理鍵を渡すのがためらわれるような状況、たとえば入社間もない社員やアルバイトの人にも鍵を渡せるのがメリット」「たしかにユースケースとしてとても現実的!」「Webカメラと組み合わせれば解錠施錠する人の顔もチェックできますね」「アルバイトの人に安心して鍵を渡せるという意味では、店舗などでも有用そう」「物理鍵持っている人がシャッターを開けるためだけに出向かなくてもよくなるのはありがたい」


後編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201110前編)Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Publickey

publickey_banner_captured

はじめての正規表現とベストプラクティス#1: 基本となる8つの正規表現

$
0
0

更新履歴

  • 2018/10/11: 初版公開
  • 2020/11/12: 追記・更新

こんにちは、hachi8833です。
BPS社内勉強会で発表したスライドを元に、正規表現を学ぶときに最初に押さえておきたいポイントをまとめました。説明ではRubyを主に使っていますが、特定の正規表現ライブラリになるべく依存しない汎用的な記述を心がけています。

正規表現に関する専門書籍やWebサイトはいろいろありますが、正規表現には唯一の正解がなく、さまざまな書き方が可能です。しかもそうした資料では機能の説明がびっしり網羅されていて、忙しい人にとって「どれが重要か」「どれが要注意か」を短時間で汲み取ることが難しくなっています。

そもそも、正規表現の書き方を単独のカリキュラムとして扱っている大学やプログラミング教室が私の知る限りでは見当たらず(学問としての正規表現はこの限りではないと思いますが)、正規表現をまとめて学ぶ機会が皆無です。

ググって目についたメタ文字を手当たり次第に使っていると、効率の悪い書き方をしてしまったり、書き方を見つけられなかったり、見つけにくいバグを埋め込んでしまったりすることがしばしばあります。

しかも困ったことに、ネット上に落ちている正規表現の解説や例にしばしば質の低いものがあります(日本語・英語を問わず)。拾い食いには要注意です。
私も質の低い情報を垂れ流さないよう気をつけないといけませんね💦。お気づきの点がありましたら@hachi8833までお願いします。

通常ならば「正規表現とは何か」から説明するところですが、第1回ではあえて正規表現のメタキャラクタ(メタ文字)を基本的かつ有用な順に並べ、順に学ぶことで自然とベストプラクティスを実現できるようにしてみました。正規表現そのものについては次回の#2で解説し、#3以降で順次高度な機能に進みます。

第1回は以下の8つのメタ文字に絞りました(メタ文字をクリックするとジャンプできます)。少なくともこれらはどの正規表現環境(ライブラリ)でも利用できます。

?
・直前の1文字が0個または1個であることを表すメタ文字
最短一致
+
直前の1文字が「1個以上」繰り返されることを表すメタ文字
{}
・直前の文字の繰り返し回数を指定するメタ文字
\
・直後のメタ文字を通常の文字として扱う(エスケープ文字)
.
・任意の1文字を表すメタ文字(ただし改行文字は除く
[]
・この記号で囲まれた文字(文字セット)のどれか1文字を表すメタ文字
()
・文字列をグループ化するメタ文字
|
・複数のパターンを列挙するメタ文字
  • 特に言及しない限り、対象文字列のエンコードはNFCで正規化済みのUTF-8とします。
  • サンプルの正規表現はRuby風に//で囲みます(囲みの/ /は正規表現そのものには含めませんのでご注意ください)。また、正規表現の断片は//で囲みません。

⚓参考: 正規表現をインタラクティブに試せるサイト

正規表現をインタラクティブに試すのに便利なWebサイトがいくつかありますので、こうしたサイトで正規表現を試しながら読み進めることをおすすめします。本シリーズでは原則としてrubular.comを使います。

  • Ruby向け: rubular.comただしRuby 2.1.5相当その後Ruby 2.5.7にアップデートされていました😂

なお、こういったサイトに限りませんが、顧客データや個人情報などを流し込まないよう注意しましょう。

⚓ 正規表現はじめの一歩: ?

?
直前の1文字が0個または1個であることを表すメタ文字

?は単純ですが、実はこれだけでも相当使いでがあります。私は心の中でこっそり「ありやなしやの?」と呼んでいます。

ご注意いただきたいのは、?は何かの文字の代用ではなく、「?直前の文字を修飾している」ことです。いわゆるワイルドカードの?とは機能が異なります(ワイルドカードとの違いについては#2で別途解説します)。

?の典型的な使い方は、カタカナの長音「ー」がある場合とない場合の両方にマッチさせたいときです。

  • 例: /コンピューター?/とすると、「コンピューター」と「コンピュータ」のどちらにもマッチします(Rubular)。

なお、マッチさせるだけなら/コンピュータ/とすれば「コンピューター」にも「コンピュータ」にもマッチします。ハイライトするとか置き換えるなどの際には/コンピューター?/とする必要があります。

なお、?のもうひとつの機能については「最短一致」で解説します。

⚓ 正規表現はじめの二歩: +

+
直前の1文字が「1個以上」繰り返されることを表すメタ文字

+も有用性の高いメタ文字です。「1個以上」なので、指定の文字がいくつ連続していてもマッチします。これも直前の文字を修飾しています。

なお、先ほどの?やこの+は、難しく言うと「量指定子(quantifier: 量化子とも)」に分類されます。要するに、直前の文字がいくつあるかを指定するものです。

  • 例: /ずうー+っと/というパターン(Rubular)。

これは「ずうっと」「ずうーーっと」「ずうーーーっと」…などにマッチしますが、長音のない「ずうっと」にはマッチしません

+は有用ですが利用にはいくつかの注意が必要です。この後で+の注意点について説明します。

⚓ 正規表現はじめの三歩: { }

{N}
直前の1文字をN回繰り返すことを表す(Rubular
{N,}
直前の1文字をN回以上繰り返すことを表す(Rubular
{N, M}
直前の1文字をN回以上M回以下繰り返すことを表す(Rubular

{ }も量指定子の一種で、回数を数値で指定できるのが特徴です。通常の文字はもちろん、文字セット[]()などを修飾して回数を指定することもできます。

{ }も有用な表現です。+だとマッチの個数に上限がありませんが、{ }だと上限を指定できるので効率が落ちにくく+よりも安心感があるので、回数を限定できるのであれば+よりも{}を積極的に使いましょう。

なお、上の例には「N回以下」がありませんが、{,N}という表現は私が知る限りではなぜかRubyでしか使えません

しかし{N, M}のパターンを応用して{1, 6}{0, 2}のように開始を1やゼロにすれば「N回以下」を表現できるので実用上は問題ありません(Rubular)。

⚓ 正規表現はじめの四歩: \

\
直後のメタ文字を通常の文字として扱う(エスケープ)

\はバックスラッシュと呼ばれ、正規表現ではバックスラッシュ直後のメタ文字の機能をキャンセルして通常の文字として扱うのに使われます。この操作を「エスケープ」と呼びます。なお、通常のスラッシュは/です。

バックスラッシュは、正規表現以外でもプログラミング言語などの文字のエスケープによく用いられます。

「1+1」という文字列にマッチさせたいときは、正規表現の中で\+とすると、+がメタ文字ではなく通常の+として扱われます。

  • 例: /1\+1/というパターン(Rubular)。

バックスラッシュ\そのものをエスケープしたい場合は、\\のように二重に表記します(Rubular)。

⚓ 正規表現はじめの五歩: .

.
任意の1文字を表すメタ文字(ただし改行文字は除く

.を単体で使うことは、意外にもそれほどありません。

そのかわり、さっきの+を組み合わせて.+とすることで、「1文字以上の任意の長さの文字列」を表すのに非常によく使われます。.+はぜひ覚えておきたい定番の表現です。

  • 例: /rails_.+\.rb/というパターン(Rubular)。

これは「rails_migration.rb」や「rails_batch.rb」など「rails_なんちゃら.rb」にマッチします。この場合、_のすぐ後ろが\.rbになっている「rails_.rb」にはマッチしません。

なお\.の部分は、任意の一文字でない「本来のピリオド文字.」を表すために先ほどのバックスラッシュ\を直前に置いてエスケープしています。こうしないと、たとえば意図しない「rails_view.erb」にもマッチしてしまいます(Rubular)。

.は、多くの正規表現ライブラリで\n\rといった改行文字にはマッチしない点にご注意ください。自分の使う正規表現ライブラリについて、このあたりの挙動を調べておくことをおすすめします。

正規表現の外からオプションを指定することで.を改行にマッチさせられるライブラリもありますが、本シリーズではこうした外からのオプションについては扱いません。

⚓ 注意: +量指定子は「最長一致」

+のような「n個以上の繰り返し」量指定子は、正規表現ではデフォルトで最大限の長さで一致する(最長一致)ので注意が必要です(ただし正規表現エンジンによっては異なるかもしれません)。言い換えると、行けるところまでめいいっぱいマッチします。

たとえば、「豚が豚をぶったのでぶたれた豚がぶったその豚をぶった豚」という文字列に/豚.+ぶった/という正規表現を適用すると、以下のようにほぼめいいっぱいマッチしてしまいます

  • 例: /豚.+ぶった/というパターン(Rubular

「豚をぶった」「豚がぶった」も含めて最長マッチしているので、マッチだけを調べていると気づきにくいバグになりがちです。このようにハイライトしたり置き換えようとしたときにバグが顕在化します。

⚓ ?のもうひとつの機能: 最短一致

最長一致では困る場合、多くの正規表現では量指定子(ここでは+)の直後に?を付けて+?とすることで最短一致を指定できます。この?は先ほどのありやなしやの?と意味の異なる、もうひとつの機能です。

なお、秀丸エディタでは最長一致を「欲張り一致」、最短一致を「ものぐさ一致」というユニークな言葉で表しています。英語でも「greedy match」や「lazy match」という用語が使われることがあります。

「豚が豚をぶったのでぶたれた豚がぶったその豚をぶった豚」という文字列に/豚.+?ぶった/という正規表現を適用すると、最短一致で3箇所にマッチします。

  • 例: /豚.+?ぶった/というパターン(Rubular)。

「豚が豚をぶった」「豚がぶった」「豚をぶった」の3箇所で最短マッチしています。

⚓ 注意: .+は使う前に考えよう

なお、「豚が豚をぶった」にはマッチさせたくないのであれば、.+ではなく、もっと精密な正規表現を書く必要があります。.+は有用なイディオムですが、このように意図しないマッチを呼び込む可能性があることを常に頭に置いておきましょう。

私?.+を使うことはめったにありません。基本的に.+は凶暴だと思っています。

⚓ 正規表現はじめの六歩: 文字セット[]

[]
この記号で囲まれた文字(文字セット)のどれか1文字を表すメタ文字

文字セット[]も正規表現の非常に有用なメタ文字です。後ろに量指定子を置いて[0123456789]+のように使うことがよくあります。

文字セット[]全体は1文字とみなされることにご注意ください。

「豚が豚をぶったのでぶたれた豚がぶったその豚をぶった豚」という文字列に/豚[がを]ぶった/という正規表現を適用すると、3箇所にマッチします。

  • 例: /豚[がを]ぶった/というパターン(Rubular

「豚をぶった」「豚がぶった」「豚をぶった」の3箇所でマッチしています。先ほどの「豚が豚をぶった」のようなマッチを排除できましたね。

文字セット[]は、数字やアルファベットに限定した文字とマッチさせるときにもよく使われます。文字セット[]の直後に+などの量指定子を置くことで、マッチの長さを任意にできます。

  • 例: /id_[0123456789]+/というパターン(Rubular)。

「id_」の直後に数字が1文字以上続く文字列にマッチします。

実は文字セット[]の中は少し特殊な世界になっていますが、今後説明する予定なのでまだ気にしなくてもよいです。詳しくは正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号をご覧ください。

⚓ 文字セット[]の注意

意外に間違えられやすいのですが、文字セット[]の中に書く文字の順序はマッチに関係ありません。

  • 例: /ぐ[りら]/というパターン(Rubular

上は「ぐり」か「ぐら」にマッチしますが、これはぐ[りら]と書いても、ぐ[らり]と書いても同じです。

ただ、文字セット[ ]の中は左から順に探索されるのが普通で、1つでもマッチすればそこで探索を終えて次の文字に進みます。これを利用して、マッチする頻度の高い文字を文字セット[ ]の左の方に置くようにすると、パフォーマンス上有利になります。

⚓ 正規表現はじめの七歩: ()

()
文字列をグループ化するメタ文字

()にはいくつかの機能がありますが、ここでご紹介するのは文字列をグループ化する機能です(他の機能は今後ご紹介します)。難しく言うと「グループ化構成体(grouping construct)」です。

先の文字セット[]では中の文字に順序がありませんが、グループ化()の場合は文字の順序が保たれます()の直後に量指定子を置くことも、()の中でメタ文字を使うこともできます。

  • 例: /(きょ)+/というパターン(Rubular

「きょ」「きょきょ」にマッチしているのがわかります。

グループ化()も単体で使うことはあまりなく、次の|とよく併用されます。

⚓ 正規表現はじめの八歩: |

|
複数のパターンを列挙するメタ文字

|は、プログラミングにおける「OR演算(論理和)」(Rubyでは||演算子)によく似ています。難しく言うと「代替構成体(alternation construct)」です。

|は、先ほどのグループ化()と組み合わせて(dog|cat|horse|cow)のようにパターンを列挙してその中のいずれかにマッチさせるのに使うことがよくあります。この場合、「dog」「cat」「horse」「cow」のいずれかにマッチします。各文字列の長さは異なっていても構いません。

  • 例: /(東京|神奈川|鹿児島)特許事務所/というパターン(Rubular)。

|は正規表現の中で処理の優先順位が低いのが特徴です。()に含まれない|は最後に解釈されます。

たとえば/東京|神奈川/とすると、「東京」または「神奈川」にマッチします

  • 例: /東京|神奈川/というパターン(Rubular

なお、(東京|神奈川|鹿児島|東京)のようにマッチする項目が(|||)に複数ある場合は、最も左側のパターンが採用されますのでご注意下さい。(|||)の中にさらに細かく正規表現を書く場合に、これをうまく利用することもできます。


|も有用なメタ文字です。複雑な正規表現でも|で上手に分割すると書きやすさ読みやすさが向上することもあります(Rubular)。無理して正規表現を一発で決めようとするより、|で分割できるかどうかを検討しましょう。

注: 上の例では今回解説しなかった正規表現((?<=))が使われています。

⚓ |の注意

|は有用ですが、以下のような点を見落としがちなのでご注意ください。

⚓ (||)+を追加する場合は注意

たとえば/(引き|抜き)にくい/とすると、「引きにくい」「抜きにくい」にはマッチするが「引き抜きにくい」にはマッチしません(Rubular)。

こういう場合、/(引き|抜き)+にくい/とすれば「引き抜きにくい」にもマッチしますが、今度は「引き引きにくい」や「抜き抜きにくい」にもマッチしてしまいます!(Rubular)。

もっと言うと、「抜き引きにくい」や「抜き引き引き抜き抜きにくい」…などにもマッチしてしまいます。

現実の日本語にはまず出現しませんが、欲しくないマッチであることには変わりありません。

これは、たとえばパターンをフレーズにして|で全体を並列する方法で回避できます(Rubular)。

「引き引きにくい」や「抜き抜きにくい」に部分マッチしているのを排除したい場合については、今後扱います。

⚓ |を余分に付けてしまう事故

()の中に|で文字列を列挙するときに、つい末尾に余分な|を置いてしまいがちです。なまじ意図しているマッチは正常に動いてしまうので案外見落とされやすいバグです。

  • 例: /(A|B|C)/と書くつもりが/(A|B|C|)/になっていた(Rubular

マッチさせるつもりのなかった「さいたま特許事務所」にまで部分マッチしてしまいました。

さらに、|の列挙が()で囲まれていないとより深刻です(Rubular)。

上は「文字ではない部分」、つまり文字と文字の間にまでマッチしてしまいました。実は直前の「さいたま特許事務所」も、「さいたま」と「特許事務所」の間がマッチに含まれているのです。

⚓ 部分マッチのワナ

詳しくは以下の記事をご覧ください。

はじめての正規表現とベストプラクティス#5(特別編)`|`と部分マッチのワナ


今回は正規表現の基本的な8つのメタ文字と利用法をご紹介しました。次回にご期待ください。

関連記事

正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号

正規表現をチョムスキー言語学まで遡って理解する(翻訳)

Ruby正規表現の後読みでは長さ不定の量指定子は原則使えない

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

皆さんはAmazon EC2 インスタンスタイプのリストをご覧になったことはありますか?このリストにはさまざまなサイズの仮想マシン(VM)が並んでいて、皆さんはこれらをレンタルしてコードを実行できます。グループごとにさまざまなサイズのVMが山ほどあるので、サイズによってグループ分けされています。

では「Railsアプリを動かすならEC2インスタンスのどのタイプで動かすべきでしょうか?」

その答えは見た目よりもシンプルです。百聞は一見にしかず。

皆さんは数字がお好きですか?私は大好きですが、数字を見たくない人は本記事の末尾までスキップしていただければ、いい感じにまとめた結論をご覧いただけます。なお、心の底から数字が好きでたまらない方のために、私が得た直接的な結果をすべて含んだ生データダンプも用意してあります。

⚓ インスタンスタイプと私のテスト方法の解説

既にご存知の方もいらっしゃるかと思いますが、私はある「巨大なRailsアプリ」を頻繁に実行しては多数のリクエストをどのぐらい高速に処理できるかをRails Ruby Bench(RRB)というパラレルベンチマークでチェックしています。

noahgibbs/rails_ruby_bench - GitHub

私は最初にEC2のどのインスタンスタイプで実行するかを検討し、それからベストなインスタンスタイプでスピードテストを実施します。

Railsアプリは驚くほど CPUに束縛されます。このことは、Ruby 2.0から2.6で72%も速度が向上した理由の一部でもあります。つまり、T4のようなバーストパフォーマンスインスタンス(burstable-CPU instance)は短期間のベンチマークでときどき目覚ましい結果を叩き出しますが、実行時間の長いベンチマークでは結果が落ちるということです。自分のアプリが極端にビジーになったり極端なアイドル状態になったりする場合は、T4を検討する価値があるかもしれません(とは言うものの、皆さんのアプリに通用する信頼できる数値を私が代わりに提供するわけにもいきませんが)。結果はそれらの負荷がどのぐらい均等であるかだけに依存します。そして、バーストパフォーマンスインスタンスではそれらの負荷が均等であるほど結果が悪くなる傾向があります。

DiscourseとRailsはメモり食いで、私はこれらのベンチマークをlower-RAM-per-CPUのC5インスタンスで回したことが一度もないほどです。もし皆さんのアプリがメモリ利用量が少ないかメモリ利用量を十分抑えられるのであれば、C5は素晴らしい選択肢になる可能性があります。Discourseやそれに近い多くのRailsアプリでは、メモリをたちまち食い尽くしてしまうでしょう。私は「標準的なRailsアプリ」ではC5をおすすめしません。しかしSinatraEventMachineFalcon/AsyncのようにRubyで別のことをやるのであれば、C5インスタンスは間違いなく試してみる価値があるでしょう。Ruby 3.0以降でRuby Ractorsが十分サポートされれば、C5インスタンスはRailsにとってもよいものになるでしょう。現時点ではほとんどのRailsアプリで必要とするメモリが不足しています。

RRBは古いバージョンのDiscourseを実行しています。Discourseはインターネットフォーラムをホスティングするアプリとして広く人気を得ているRailsアプリであり、入手できる「本物の」オープンソースRailsアプリとしては極めて巨大なので、「現実世界」のベンチマークに向いています。RRBは、疑似乱数でシミュレートした多数のユーザーリクエストのセットをRailsアプリに対して実行し、すべてのリクエストが完了するまでの時間を測定します。つまりスループットテストです。ご興味のある方は 自分でも実行できますが、少々込み入っていて注意を要します。現実世界のソフトウェアを使う場合、現実の複雑さとバグを踏むことが暗黒面になります。

GIL(RubyではGVL)が存在しているので、RRBを実行するときには「マルチプロセス」と「プロセスごとのマルチスレッド」のバランスを取っています。多くのスレッドがI/OやCベースのネイティブ拡張のコード実行を待てるにもかかわらず、1個のRubyプロセスではRubyコードのシングルスレッドしか実行できません。Railsの場合、1プロセスあたり5スレッド程度にバランスさせるのが普通です。なお私はRRBの特定のケースではごく小規模な改善として5よりも6を使います。DiscourseでやっているI/O待ちの量だと、EC2 2xlargeインスタンスのvCPUとRAMは基本的に10個のRailsプロセスで使い切ります。つまり「10プロセス、6スレッド/プロセス」です。

RRBが実行するコードはかなり古いものです。私がRRBを設計したのはRuby 3×3移行をベンチマークするためだったので、古いコードとの互換性を優先しています。2020年のクリスマスの日にRuby 3がリリースされたら、私は片っ端から最新バージョンにアップグレードして回るでしょう。古いバージョンはそのときまで残しておくつもりです。なおM6g ARM インスタンスの出番はまだです。古いRubyをそこでビルドすることならできますが、古代から伝わるNodeJSやlibv8などの他の古いコードもそこで動くようにするのは本当にしんどい作業でした。

もしM6gインスタンスを試してみたいのであれば、エンジニアリングにある程度余分な時間が必要になります。Intelプロセッサーはあらゆるものでデフォルトになっているので、おそらく動かすのも実行を維持するのも手こずるでしょう。M6gインスタンスをうまく活用するのに十分なサーバー予算をお持ちなら、存分に自分で測ってみてください。ただし私のベンチマークで測定が楽になるわけではありません

まとめると、「Tシリーズ」「Cシリーズ」「M6gシリーズ」のインスタンスは、特殊なRailsアプリには向いている可能性もあります。私は一般にこれらのインスタンスを万人にはおすすめしませんし、これらのスピードテストを行ったこともありません(これらのインスタンスはたいていのRailsアプリでは間違った解だからです)。

というわけで、残る選択肢は「主にM4およびM5インスタンス」です。M4のマシンとアーキテクチャは古いのですが、M4とM5の価格は似たようなものです。

⚓ 「スポットインスタンス」vs「オンデマンドインスタンス」

AWS EC2には標準価格のオンデマンドインスタンスがあります。スポットインスタンスについては安く手に入る可能性もあればできない可能性もあり、ちょうど航空チケットを離陸直前に買うときのように価格は運次第です。運がよければ驚くほど低価格な未使用の容量を余分にゲットできますが、ダメなときはダメです。

そこで、この新しいインフラストラクチャ(M5)がどのぐらい高速であるかを知っておくと、M5をお手頃価格に比例して入札できるようになるので便利です。

AmazonはM4ユーザーが他のインスタンスタイプへ移行することを望んでいますが、M4スポットインスタンスはタイミング次第では安く手に入れられる可能性がまだ残されています。M4のコストパフォーマンスはどのぐらいで、どんな場合に向いているのでしょうか。

⚓ M4はシンプルか?

私は何年もかけて、主にm4.2xlargeでベンチマークを回してきました。同じインスタンスタイプを使えば長期間での数値の比較がやりやすくなります。私は本記事でさまざまなバージョンのRubyをM4とM5でチェックしています。スピードが単純に「x倍高速」となることはめったにありませんが、複数のファクターをテストすることで自分の測定結果がどのぐらいシンプルで安定しているかを見極めるのに役立ちます。

私がEC2の4つのインスタンスタイプを用いて最終的に行った測定では、Ruby 2.5.3とRuby 2.6.6をテストしました。また、スピードがどのぐらい変動するかについて当りを付けるために、それ以外の多くのインスタンスでも最初に実行してみました。同一のベンチマークを3年間回し続けていてひとつよかったのは、「通常の」安定性および変動がどのような状態なのかをかなり体感でわかってきたことです。舞台裏の数値を詳しく見てきた分、本記事ではちょっぴり細部をごまかしているところもありますが。さて本記事にある項目のうち、それを5倍〜100倍に増やさなくてもよい項目は何だと思いますか?そう、測定値です。

また、RRBは安定した数値を得られるよう、いい感じに最適化されています。RRBは同じ乱数シードを繰り返し用いて生成した同一のリクエストを実行します。RRBのリクエストは十分小さくかつ高速で、しかもハードウェアのネットワークを一切使いません。リクエストはすべてlocalhostです。EC2のように仮想化された環境ではハードウェアネットワークが不安定性の「大きな」原因となるので、単純にスキップしています。もちろん、どのインスタンスも同じスピードになるはずはありませんが、スピードのばらつきはハードウェアネットワークの場合よりもずっと小さくなります。

EC2では「ハードウェア専有インスタンス(dedicated instance)」の利用も認められており、そこでは同じ物理ハードウェアを他の誰にも共有されないようにできます。私はもう何年もハードウェア専有インスタンス上でテストを繰り返しています。ハードウェア専有インスタンスではある種の時間変動については「確実に」回避できますが、私の経験ではハードウェア専有インスタンスでも未だに速くなったり遅くなったりします。そのため、この特定の測定項目セットでは大きな利点はありません。

⚓ 基本は「M4」

要するにM4インスタンスはどんな具合になるのでしょうか?

Ruby 2.5 ips Ruby 2.6 ips Ruby 2.6の速度差
m4 inst 1 168.9 175.3 +3.8%
m4 inst 2 156.8 164.0 +4.6%
m4 inst 3 169.2 176.8 +4.5%
m4 inst 4 167.4 175.6 +4.9%
m4 overall 167.4 175.3 +4.7%

(「ips」は10000件のHTTPリクエストを30回実行したときの「イテレーション/秒」のメジアン、
「m4 overall」は全リクエストを1回の長時間実行として扱うことを表す)

インスタンス2が目に見えて遅くなっています。最速のインスタンス3は(インスタンス2よりも)およそ7.8%高速です。しかしこれがまったくのランダムではないことがわかってきます。そのインスタンスはRuby 2.6で7.8%高速、Ruby 2.5で7.9%高速です。これが今回のケースで観察される「安定性」です。インスタンスによってわずかに高速なこともあれば、わずかに遅くなることもありますが、相対的な数値は類似しています。それと同様に「このタスクにおけるRuby 2.6はRuby 2.5と比べて3.8%〜4.9%高速」という結果にも若干のばらつきがありますが、このベンチマークにおけるばらつきの総量はかなり正常です。

私はこうした結果を何年も見つめ続けてきました。そしてこれは典型的な結果セットです(統計的に有意なミスでもない限り: もちろんたまにはミスぐらいしましたが)。このトピックに関する私の研究を追いかけている方なら、おそらく何年にも渡って同じ結果が出ていることに気づくでしょう。

ちょっと待った、あるインスタンスが他のより速いことがあるってマジで?」とお思いの方へ: 答えはイエスです。私は、EC2オンデマンドインスタンスの巨大なグループを立ち上げてベンチマークを回し、その中で最も遅い10%をシャットダウンしている人々を知っています。そしてこれはCPUよりもネットワークの方がずっと顕著です。私がEC2ベースのメトリクスでlocalhost以外のネットワークを含めないようにしている理由がこれです。

⚓ M4とM5を比較する

今のはすべてM4インスタンスの話でした。ではM5インスタンスの数値はどうでしょうか?ここは本記事で費用対効果を比較できる有用な部分です。

Ruby 2.5 ips Ruby 2.6 ips Ruby 2.6の速度差
m5 inst 1 206.5 213.3 +3.3%
m5 inst 2 200.7 204.7 +2.0%
m5 inst 3 203.7 213.8 +5.0%
m5 inst 4 214.1 223.5 +4.4%
m5 overall 206.1 214.4 +4.0%

(「ips」は10000件のHTTPリクエストを30回実行したときの「イテレーション/秒」のメジアン、
「m4 overall」は全リクエストを1回の長時間実行として扱うことを表す)

こちらのインスタンスの結果はややばらつきが小さくなっています。最速のインスタンスは最も遅いインスタンスより6.7%高速です。Ruby 2.5とRuby 2.6を比べた数値には依然として多少ばらつきがあるものの、overallはM5インスタンスが4.0%、M4が4.7%と似ています。言い換えると、これはすべて正常なばらつきです。

インスタンス2が+2.0%と差が小さいのはどういうことでしょう?一方はうまくいって他方は不運に見舞われることもあります。そしてRuby 2.5と2.6はたまたまそのインスタンスでそこそこ近い結果になったのです。舞台裏を覗いてみても、これは大きな異常値でもなければ突発性のスローダウンでもありませんでした。そのインスタンスでの実行が通常よりも(高変動ではなく)低変動だったのです。これは、Ruby 2.6がRuby 2.5に対して持つ長所を目減りさせる「遅いEC2インスタンス」の特殊なケースのように見えます。おそらくI/O待ちが(極めて)わずかに長くなったか、CPU時間が(極めて)わずかに長くなったかしたのでしょう。言い換えると、それが何であれ、スローダウンが1分間も続くような一時的なしゃっくりなどではなく、長期的には個別のEC2インスタンスのマイナーなスローダウンと思われます。

⚓ 費用対効果

とにかく、M4インスタンスのメジアンスループットは、Ruby 2.6で175.3リクエスト/秒となり、最新のRubyとほぼ同じスピードです。そしてM5インスタンスのメジアンスループットは、同じ設定で214.4リクエスト/秒となります。これは一体何を意味しているのでしょうか?

手っ取り早く言えば、M5インスタンスの方が36%高速ということです。つまり価格設定をスポットインスタンスにするとしたら、M4インスタンスはM5インスタンスより36%安くなければ割りに合わないということになります。

ではコストはざっくりどのぐらいになるでしょうか?

いいですか、ここが重要です。オンデマンドの場合、実際にはM5インスタンスの方がM4インスタンスよりも時間当りのコストが安いのです。つまり費用をすべてオンデマンドに振り向けるのであれば、細かい比較などしなくても結論は「M5一択」で決まりです。M4インスタンスを一度もアップグレードしたことがないなら、今こそアップグレードすべきタイミングです。us-east-1のオンデマンドm4.2xlargeインスタンスは米ドルで40セント/時間ですが、m5.2xlargeは38.4セント/時間となり、(訳注: スピードの差を加味すると)M4の方が4.1%高くつきます。

つまりM5がM4よりどれだけ高速かを計算して初めて、M5のディスカウントが大きいことが見えてきます。これホントにホント。

また、以下の記事やこれらの数値は次のことも示唆しています。もし小さな速度差を最適化しているのであれば、最近のRuby(少なくとも2.6以上)にアップグレードしましょう。Ruby 2.6は2.5に比べて著しく改善されており、以下の記事にあるようにそれ以前のRubyと比べてかなり速度に差が付きます。

私たちはRuby 3.0の速度がどうなるかをいずれ目にすることになります。以下の記事にあるように、数か月前にリリースされたRuby 3.0プレビュー版は2.7と比べるとそこまで速度差は大きくありませんが。

⚓ まとめ

忙しい人向け: EC2インスタンスはM5シリーズを使いなさい、以上。私はvCPU数やRAM容量の点でm5.2xlargeが好みです。なお、必要に応じてスケールアップやスケールダウンしてもvCPU数とRAM容量の比率は変わりません。かなり特殊なケースであれば、M6g(ARMプロセッサ、移植が困難、CPUは高速)や、C5(CPU1個当りのRAMが少なめ)、T4(CPUをバーストパフォーマンスで動かせる)を検討してもよいかもしれません。

しかし普通のRailsアプリのユースケースであればやはりM5があなたの友です。「でもM4スポットインスタンスは安いし」とお思いのそこのお方、M4が価格性能比でM5と互角に勝負するには、少なくともM5より36%安くなければいけないことをお忘れなく。それでもM4を使いたい方はどうぞ慎重に。

⚓ 「どうも信じられん」

まだ納得いただけませんか?公平のために申し上げると、私のコードはすべてGitHubで公開されていることにお気づきでしょうか?ドキュメントを読んで自分で動かすのも自由ですし、インスタンス数を増やすなどして再実行して、自分の環境でも私と同じ結果になるかどうかを見たい方は、@codefolioまでお声かけいただければ、私の行った正確な方法を喜んでお見せいたします(コードもすべてネット上にあります)。私のデータを使えば皆さんが自分でEC2インスタンスをレンタルするより安上がりです。

⚓ お知らせ

パフォーマンスについて詳しく知りたい方は、FastRuby.ioブログのperformanceタグにある記事をご覧ください。

関連記事

Ruby 2.5.0はどれだけ高速化したか(翻訳)

ベンチマークの詳しい理解と修正のコツ(翻訳)

macOSのアップデート失敗後にダウンロード前の状態に戻す

$
0
0

更新履歴

  • 2018/04/20: 初版公開
  • 2020/11/13: 追記、更新

こんにちは、hachi8833です。

macOSのアップデートに立て続けに失敗し、その後復旧したときの方法をメモします。本記事はmacOS High Sierra時点で執筆しました。

ファイルシステムが壊れるという、より深刻な事態の場合は、起動しなくなったMacからネットワーク経由でデータをサルベージするもご覧ください。

⚓ 症状

自分の場合はESET CyberSecurity ProにせかされてmacOS High Sierra 10.13.4アップデートをインストールしようとして発生しました。

もちろん、TimeMachineでのバックアップ完了を確認してから始めました。

App Storeアプリでのダウンロードがやけに時間がかかるなと思いつつ、やっと終わって再起動すると、黒背景のプログレスバーが表示されて10分ほど経過した後でこんな画面が出てきました。

せっかくなので検索用にメッセージをメモしておきます。

コンピュータにmacOSをインストールできませんでした
macOSのインストール中にエラーが起きました。Apple Diagnosticsを使ってMacハードウェアをチェックするには、シャットダウンしてから、電源ボタンを押し、すぐにDキーを押したままにして、診断が開始されるまで待ってください。
インストーラを終了してコンピュータを再起動してからやり直してください。
[再起動]

おそらく何らかの原因で、ダウンロードしたファイルに傷が入ってしまったのでしょう。

⚓ 1. macOSを復旧する

ここから取れる道はいくつかあるといえばありますが、まずはmacOSを復旧します。

  • 起動後にDキーを押したままにして、自動診断を走らせる(これを最初にやるべき)

  • ハードウェアに問題が生じていたら、諦めてGenious barに持っていくなどする

  • ハードウェアが無事なら(自分の場合はこれ)、自動診断画面の[再起動]をクリックして再起動
  • 再起動直後にコマンドキーとRキーを同時にしばらく押し続け、復旧用の画面を出す

  • [TimeMachineバックアップから復元]を選ぶ

  • [続ける]を選ぶ

⚓ ローカルスナップショットを活用しよう

一度目の修復では、この[続ける]の次の画面でTimeMachine(Wi-Fi越し)からバカ正直に復旧をかけてしまったので、6時間ぐらいつぶしてしまいました。

二度目の修復で復旧元としてローカルスナップショットを選択してみたら、3分もかからずに復旧しました。ローカルスナップショット、偉い!

後はほぼ秒殺とも言える速度で復旧します。それなりに余分な容量を食うのでしょうけど、ローカルスナップショットありがたいです。

ローカルスナップショットはHigh Sierra以降でないと使えませんのでご注意ください。

⚓ 参考: ローカルスナップショットをコマンドラインで確認する方法

ターミナルで以下のコマンドを実行すれば、Macを再起動せずにローカルスナップショットの一覧を確認できます(6桁の数字は時分秒です)。root権限は不要でした。

$ tmutil listlocalsnapshots /
# 実行例
~$ tmutil listlocalsnapshots /
Snapshots for volume group containing disk /:
com.apple.TimeMachine.2020-11-12-185058.local
com.apple.TimeMachine.2020-11-12-203437.local
com.apple.TimeMachine.2020-11-12-213628.local
com.apple.TimeMachine.2020-11-12-223439.local
com.apple.TimeMachine.2020-11-12-234307.local
com.apple.TimeMachine.2020-11-13-010433.local
com.apple.TimeMachine.2020-11-13-060152.local
com.apple.TimeMachine.2020-11-13-070218.local
com.apple.TimeMachine.2020-11-13-083600.local
com.apple.TimeMachine.2020-11-13-093155.local
com.apple.TimeMachine.2020-11-13-103205.local

参考: macOSのローカルスナップショット - panda’s tech note

⚓ 2. ダウンロードしたアップデートを消してやり直す

追記(2020/11/13): macOS Catalina(おそらくMojaveも)ではこの方法でアップデートファイルを削除する前にSIPを無効にする必要があります。削除した後はSIPを再び有効にしましょう。
参考: システム整合性保護(System Integrity Protection: SIP)の無効化 – Intego Support

ここからが本題です。
ローカルスナップショットのおかげでひとまず再起動前の状態には戻りましたが、ダウンロードしたファイルに傷が入ったままでは再起動するたびに同じ目に遭ってしまうので、何とかしてダウンロードからやり直したいところです。

ここからは以下の方法が使えます。

要するにダウンロードファイルを消してしまえば、ダウンロードをなかったことにしてくれるそうです。

なお、アップデートをダウンロード中にMacがスリープすると失敗しやすいようです。ダウンロード中はスリープを止めておくことをおすすめします。

⚓ GUIで削除する場合

Finderから消す場合は、Finderの[移動] > [フォルダへ移動]メニューを開き、/ライブラリ/Updatesと入力してフォルダを開き、そこにあるファイルを全部消し、再起動します。

これで再起動後にApp Storeを開くと、アップデートがちゃんとダウンロード可能になっています。

⚓ 参考: コマンドラインで削除する場合(経験者向け)

「ターミナルでsudo rm -rf /Library/Updates/*を実行して再起動」でできます(root権限が必要でした)。

危険を伴う可能性のあるコマンドなので、入力ミスにはくれぐれもご注意ください。

参考: 【絶対にやってはいけないLinuxコマンド「# rm -rf /」をやっている動画みつけた】確認無しで指定されたパス以下全てのファイルを削除 | 今村だけがよくわかるブログ

関連記事

起動しなくなったMacからネットワーク経由でデータをサルベージする

Mac: homebrew caskを一括アップデートできるhomebrew-cask-upgrade

Mac+HomebrewでPostgreSQLが起動しない場合の対応


[DOM] Rangeを作りすぎて激重になった話

$
0
0

DOMの規格にはRangeというクラスがあります。ドキュメント上の選択範囲などを表すのに便利なクラスです。
ついさっき、このクラスにまつわるパフォーマンス問題を解決したので記事に残そうと思います。

removeChild()が重い?

とあるDOM操作を行う処理のパフォーマンスが悪い、というチケットが立てられたのが発端でした。
最初にその部分を実装したのが私で、そこまでチューニングをしっかりしていた訳ではなかったのでまあそんなこともあるかな、と思いながらとりあえずパフォーマンス計測を行ってみたところ、appendChild()が実行時間の9割以上を占めているという結果でした。

appendChild()がなんでそんなに遅いんだろう?
appendChild()は親ノードの子のリスト末尾に1個ノードを追加するだけの処理です。普通に考えてこれだけではそこまで遅くなるとは思えない。
appendChild()と言っても、あるノードの中から別のノードに移動する処理だったので、内部ではノードを外す処理とノードを追加する処理が行われることになります。そこで、実際に重いのがどこなのか突き止めるため、removeChild()を行ってからappendChild()を行う処理に変更すると、removeChild()appendChild()よりずっと重いという結果が出てきました。

(同じような状況の再現コードでパフォーマンス計測を行ったもの)

その時点では、removeChild()appendChild()にここまで大きな差が出る理由は見当がつきませんでした。

const span = document.createElement('span');
const text = document.createTextNode('test');
const t = Date.now();
for(let i = 0; i < 100000; ++i) {
  span.appendChild(text);
  span.removeChild(text);
}
console.log(Date.now() - t);

実際、上記のような、単にappendChild()removeChild()を繰り返すだけの単純な計測コードだと以下のような結果になり、removeChild()が若干重いかという感じではあるものの、せいぜい2倍程度の差です。

同じように計測コードを問題の起きているページにも埋め込んで実行してみましたが、それだけではappendChild()removeChild()の実行時間はやはり同じでした。
ところが、チケットで問題となったDOM操作を行うと、その後同じ計測コードの実行でremoveChild()が重くなるのです。不可解な現象です。

Rangeが原因だった

まあタイトルにある通りなので当然なのですが、問題の発生する処理は実行中に何個もRangeを作成していました。
そのRangeを作成している箇所の前で実行を中断した場合と、その後まで実行した場合とで、計測コードのパフォーマンスに大きな差が生まれました。つまりRangeを作るとパフォーマンスが悪くなる訳です。
ここには、RangeLive(生きている)オブジェクトであることが関わっていました。

Live(生きている)オブジェクトとは

DOMの仕様には、いくつかLiveという言葉が登場します。例えば、HTMLCollectionはMDNの説明を見てみると、

HTML DOM 内の HTMLCollection は生きて (live) います。それらは元になった document が変更された時点で自動的に更新されます。

という文言があります。HTMLCollectionは例えばgetElementsByClassName()の戻り値ですが、これが生きているというのがどういうことかと言うと、DOMツリーが更新されると勝手に中身が変わるということです。
getElementsByClassName()で取り出したHTMLCollectionなら、そのクラス名を持たない要素は含まれないので、クラス名を付けたり外したりするとHTMLCollectionの中身も増えたり減ったりする訳です。

Node.childNodesなどもNodeListのLiveオブジェクトですが、Rangeもまたそれらの一種です。
ただ、getElementsByClassName()childNodesなどで扱うHTMLCollectionNodeListは、そもそもDOMツリーが内部的に持っている配列を単に参照しているだけのものなので、DOM構造が変われば自動的に更新されますが、更新にかかる追加のコストはありません。
一方、Rangeが生きているというのはどういうことかというと、DOMツリーが変更される度に、そのドキュメントに関連付けられたRange全てに関して、変更を反映する必要があります。つまり、更新が発生した時に生きているRangeが多ければ多いほど、更新処理が重くなるということです。

Rangeの存在がremoveChild()のパフォーマンスに影響を与えてappendChild()に影響しない理由は、おそらくですがappendChild()Rangeの指す位置が変わることがないからでしょう。Rangeが指すのはノードとそのオフセット(親要素なら子ノードのリストの位置、テキストノードならテキスト位置)ですが、appendChildは子ノードのリストの末尾に追加するだけなのでそれらが変わることがありません。一方、removeChild()の場合は、Rangeが指しているノードそのものが取り外されたり、Rangeの指すオフセットより前のノードが外されてオフセットがずれたりするので、変更を反映する必要が生まれるのです。

Rangeオブジェクトと言えどいつかはGCにかけられるのだから、どこからも参照されなくなればいつかは死にます。とは言え、実際に問題が発生している以上、GCに回収されないままのRangeが残っていて、それがまだ生きている扱いになっていることは想像に難くありません。

Rangeの殺し方

Rangeが生きているのが原因なのだから、殺してしまえ。
私の中の首領(ドン)が囁きました。何の組織の首領なのか、それは分かりません。

さて、Rangeにはdetach()という、いかにもそれっぽいメソッドがあります。MDNにはこう書いてあります。


developer.mozilla.orgより

なるほどこれを使えばいいのか!

残念でした。
detach()は使えません。WHATWGのLiving Standardにおける定義の方を見てみれば

The detach() method, when invoked, must do nothing.
DOM Standardより

と書いてあります。MDNの方でも個別ページを見に行けば同じことが書いてあります。つまり、これは互換性のために残された機能で、昔は使えたかもしれませんが今では使えない訳です。

ではどうすれば良いのだろう?

detachはそもそも何をやっていたのか、同じような処理を発生させることはできないのか。
それを探るため、調査班はアマゾンの奥地Chromiumのソースコードに飛んだ。

void Range::Dispose() {
  // A prompt detach from the owning Document helps avoid GC overhead.
  owner_document_->DetachRange(this);
}

Dispose()というのは、ちゃんと確認した訳ではないですがおそらくGCがオブジェクトを削除する時に呼び出す関数のはずです。
owner_document_->DetachRange(this); というのはいかにもそれっぽいですね。おそらく昔はこの処理がdetach()で公開されていたのでしょう。
DetachRange()の処理対象オブジェクトはowner_document_なのですね。owner_document_は要するにRangeが指すノードのownerDocumentと同じものでしょう。

はて。ということは。
RangeownerDocumentの異なるノードを指すようにすれば、owner_document_が切り替わるのではないか?
owner_document_が切り替わる時に、DetachRange()が呼ばれるのではないか?
実際、setStart()などのコードを追ってみますと、owner_document_が異なる場合に更新をかける処理(SetDocument())が呼ばれています。

if (ref_node->GetDocument() != owner_document_) {
  SetDocument(ref_node->GetDocument());
  did_move_document = true;
}

つまり、Rangeを完全に殺すことはできなくても、ダミーのDocumentを作ってそちらを指すようにすることは可能なのではないでしょうか。

という訳で、こんなコードを用意してみました。

let dummyDocument: Document | undefined;
export function releaseRange(range: Range) {
  dummyDocument = dummyDocument || document.implementation.createDocument(null, 'dummy', null);
  range.setStart(dummyDocument, 0);
}

Rangeを利用していた部分で、もう使わなくなったRangeに関して全てreleaseRangeを呼ぶようにしてみました。
releaseとか言いつつ別にここですぐにRangeオブジェクトが削除されるわけではないのですが、これ以降再利用しないことを示したかったのでこういう名前になっています。

この関数を呼ぶとRangeオブジェクトはdummyDocumentにアタッチされ、ページのdocument以下のツリーの変更があっても更新されません。そのうちGCに回収されるはずです。

それで実際に挙動はどうなったかと言うと、見事にパフォーマンスが改善されました。めでたしめでたし。

実際にRangeを増やした時に時間がかかることを体感できるよう以下のサンプルを用意しました。試してみてください。
1000くらいの値を入れれば十分な差が出ます。あまり大きな値を入れるとRangeを作成するだけでメモリが大量に必要になるので注意してください。



関連記事

TypeScriptにヤバい機能が入りそうなのでひとしきり遊んでみる

週刊Railsウォッチ(20201116前編)6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。ドキュメントの更新が増えているようです。

PredicateBuilderで委譲をサポート

このコミットより前は、where(id: user)where(author: user)オブジェクトを値として指定するクエリがシリアライズに成功するのは、値(この例ではuser)がActiveRecord::Baseの子孫のインスタンスである場合だけだった。
(SimpleDelegatorやdelegate_missing_toやその他の方法で)デコレーターを用いるアプリケーションでは、この変換が必須なのが少々つらくなることがある。
このコミットはPredicateBuilderを変更して、基本となる比較条件を#respond_to?呼び出しに置き換える。
同PRより大意


つっつきボイス:「where(id: user)where(author: user)のようにwhere()にActive Recordオブジェクトを渡すことができるんですが、デコレーターを通す場合は変換しないといけなかったのか」

「Changelogのコード例↓はいわゆるPORO(Plain Old Ruby Object)で、delegate_missing_to :@authorを書いておけばその後のPost.where(author: AdminAuthor.new(author))が動くように変更されたのね」

#idと主キーを委譲するオブジェクトを用いてpredicate条件をビルドする。
Changelogより

# Changelogより
class AdminAuthor
  delegate_missing_to :@author
  def initialize(author)
    @author = author
  end
end

Post.where(author: AdminAuthor.new(author))

参考: ruby - How can I determine if an object is a PORO or not? - Stack Overflow

「変更部分を見ると↓、修正前のif value.is_a?(Base)はActive Recordの場合だけ処理を進めるけど、value.respond_to?(:id)に変えたことで#idメソッドを持っているかどうかでチェックするようになった」「なるほど、Active Record以外のオブジェクトでも#idメソッドがあれば動くんですね」「どのぐらい使われるかはわかりませんが、やりたいことは見えてきました」

# activerecord/lib/active_record/relation/predicate_builder.rb#L61
    def build(attribute, value, operator = nil)
-     value = value.id if value.is_a?(Base)
+     value = value.id if value.respond_to?(:id)
      if operator ||= table.type(attribute.name).force_equality?(value) && :eq
        bind = build_bind_attribute(attribute.name, value)
        attribute.public_send(operator, bind)
      else
        handler_for(value).call(attribute, value)
      end
    end

has_one :throughでコンストラクタを使えるようになった

これに関する参考情報はガイドやドキュメントに見つからなかったが、has_one :through関連付けの関連付けコンストラクタが無効になっているように見える。自分は単にこれをテストで有効にしたところ、何も壊れなかったので、これを無効にすべき理由が何なのかを考えている。そのあたりがわかる情報をもらえると嬉しい。また、無効にすべき十分な理由がある、この変更に重要性がない、または不要なリスクがあるならば、ドキュメントを更新してhas_one :throughではbuild_associationcreate_associationを利用できないことを反映すべき(もしそうならこのPRの代わりに自分がドキュメントを変更してもよい)。
同PRより大意


つっつきボイス:「今まではhas_one :throughのときにbuild_associationcreate_associationなどのコンストラクタが使えなかったのか」「今までできなかった理由が謎」「使ってはいけないという記述がドキュメントに見当たらなかったので実装したそうです」

「テストコードでもコンストラクタのビルドをチェックしている↓」

# activerecord/test/cases/associations/has_one_through_associations_test.rb#L71
+ def test_association_build_constructor_builds_through_record
+   new_member = Member.create(name: "Chris")
+   new_club = new_member.build_club
+   assert new_member.current_membership
+   assert_equal new_club, new_member.club
+   assert_predicate new_club, :new_record?
+   assert_predicate new_member.current_membership, :new_record?
+   assert new_member.save
+   assert_predicate new_club, :persisted?
+   assert_predicate new_member.current_membership, :persisted?
+ end

「なるほど、calculate_constructableメソッドで:throughを止めてたのね↓」「:throughの場合はコンストラクタブルにならないようにわざわざチェックしてたとは」「ホントだ」「このメソッドがあった理由は謎ですが、消したらできるようになったのでプルリクしたんでしょうね」

# activerecord/lib/active_record/reflection.rb#L693
-
-     private
-       def calculate_constructable(macro, options)
-         !options[:through]
-       end

has_one :throughはあまり書かなさそうだけど、使いたいときに使えるようになったのはいいと思います」

開発時のルーティングをリロードフックの後でappendするようになった

appendしたrootルーティングが内部のwelcomeコントローラより優先されるようにする。
自分が解決しようとしているこの特定のユースケースは「rootルーティングがないアプリ」と「rootルーティングをappendするエンジン」の組み合わせである。ルーティングファイルは初期段階ではここで読み込まれるが、welcomeコントローラはここで追加されているらしい。これはつまり、現在はビルトインのルーティングがadd_builtin_routeで追加されるより「前に」実行されるエンジンイニシャライザを作成しないといけないということになる。このパッチによって、ビルトインのルーティングはルーティングファイルが評価された「後で」アプリケーションに追加されるようになる。
同PRより大意


つっつきボイス:「welcomeコントローラは、Railsのdevelopmentモードでデフォルト表示される例のWelcome画面を出すヤツですね」

「なるほど、developmentモードでビルトインのルーティング処理を移動してroutes_reloader.executeを追加している↓」「Railsエンジンとの兼ね合いでデフォルトのrootルーティングの読み込み順がうまく設定できなかったのを、ユーザー定義のルーティングを先に設定してからビルトインのルーティングを設定するように修正したらしい」「appendしたrootがそれによって内部のwelcomeコントローラより優先されるようになったんですね」

# railties/lib/rails/application/finisher.rb#L198 (移動前との差分を表示しています)
      initializer :add_builtin_route do |app|
        if Rails.env.development?
          app.routes.prepend do
            get "/rails/info/properties" => "rails/info#properties", internal: true
            get "/rails/info/routes"     => "rails/info#routes", internal: true
            get "/rails/info"            => "rails/info#index", internal: true
          end

          app.routes.append do
            get "/"                      => "rails/welcome#index", internal: true
          end

+         routes_reloader.execute
        end
      end

「たしかRailsのルーティングは先に読み込まれたものが有効になったと思うので、それも関連していたのかもしれませんね」

参考: Rails のルーティング - Railsガイド

Railsのルーティングは、ルーティングファイルの「上からの記載順に」マッチします。このため、たとえばresources :photosというルーティングがget 'photos/poll'よりも前の行にあれば、resources行のshowアクションがget行の記述よりも優先されますので、get行のルーティングは有効になりません。これを修正するには、get行をresourcesよりも上の行に移動してください。これにより、get行がマッチするようになります。
Railsガイドより

アップグレードガイドにimage_processingセクションを追加

Active Storageでimage_processing gemが必須になった

Active Storageでvariantsを処理するときに、mini_magickを直接用いるのではなくimage_processing gemをバンドルすることが必須になりました。image_processingはデフォルトでmini_magickを背後で用いるよう構成されるので、アップグレードは(訳注: Gemfileの)mini_magick gemを単にimage_processing gemに置き換えて、既に不要になったcombine_optionsが明示的に使われているのを削除しておくのが最も簡単です。
ただし、素のresize呼び出しをimage_processingのマクロに変更することが推奨されます(リサイズ後のサムネイル画像もシャープになるので)。

video.preview(resize: "100x100")
video.preview(resize: "100x100>")
video.preview(resize: "100x100^")

たとえば上をそれぞれ以下のように置き換えます。

video.preview(resize_to_fit: [100, 100])
video.preview(resize_to_limit: [100, 100])
video.preview(resize_to_fill: [100, 100])

同PRより大意

janko/image_processing - GitHub


つっつきボイス:「アップグレードガイドに追加された注意書きです」「以前Railsに導入されたimage_processing gemが6.1からは必須になったのか(ウォッチ20180511)」「これまではmini_magickかimage_processingのどちらかを使えばいいという感じだった気がします」「今後はmini_magickを使いたければimage_processing経由で使ってくれということか」「これまでmini_magickを素で使ってた人は修正が必要になりますね」

minimagick/minimagick - GitHub

Rails

onkさんの「Smart UIパターン」記事


つっつきボイス:「はてブで大バズリしていたonkさんの記事がRailsにも言及していたのでピックアップしました」「そういえば最近のTwitterでテーブルデータゲートウェイの話題を見かけた気がするけど、もしかするとこの記事がきっかけだったのかも🤔」

参考: テーブルデータゲートウェイ

「記事にも登場している『エンタープライズアプリケーションアーキテクチャパターン』(通称PoEAA)↓はやっぱり名著」「記事でもPoEAAに登場したトランザクションスクリプトやアクティブレコードやデータマッパーなどのいろんなパターンをおさらいしていますね」

エンタープライズアプリケーションアーキテクチャパターンを読む: 1.概要

「そしてRailsの密結合設計の話と@_yasaichiさんの名スライド『Ruby on Railsの正体と向き合い方も扱われていますね(ウォッチ20190401)』」「Railsは最短期間で開発できる代わりにその構造のまま大きくなるとすごく大変になる、はそのとおりだと思います」「Railsほどのラピッド開発を他のフレームワークでやるのは大変そう」

「記事にある『本当に複雑なものと、複雑ではあるが工夫で対処できるもの』という見出しはわかりやすい切り口ですね」「おぉ」「本質的に複雑なものもあるけど、複雑なりに対処できるものもあるという話」「おや、TechRacho記事も参照されている」「はい、それでこの記事に気づきました」

本当に複雑なものと、複雑ではあるが工夫で対処できるもの
同記事見出しより

「そして複雑さに対処する方法がいくつかリストアップされていますね: TrailblazerのOperationや、HanamiのInteractorなど」「rectifyは見たことなかった」

andypike/rectify - GitHub

「その次の『同機能の CUI コマンドを作るときに重複が無い』のパラグラフ↓もいいですね: 言い換えるとコントローラがコントローラの役割だけを担うようになっている」「なるほど」

Web アプリケーションとしてのインタフェース (View 以外) が Controller の役目で、「同機能の CUI コマンドを作るときに重複が無い」という基準で考えると良い。
同記事より

「『Controller or Action と 1:1 対応する新しい層』という考え方も流行りましたね: この場合ファイル数がものすごく増えてしまうんですが、そこはもう仕方がない」

「System on Record(SoR)とSystem of Engagement(SoE)という考えも見たことある↓」

「『SoEの戦場がクライアント側に移っている』もわかる: ユーザーとのインタラクション処理をフロントエンド側に移して、そこでGraphQLのインターフェイスが使えればいいという感じでやることが増えているように思います: その分SoRの複雑さについては引き続きサーバーサイドエンジニアが面倒を見る必要はあるでしょうね」


「この記事も含めて、最近の設計関連記事の多くが似たような結論に達しているのが面白いですね: たとえばActive Recordはあまりにもよくできていて、小さい問題を解決するときには非常によい、とか」

「その一方でフロントエンドでGraphQLを使うと割と手軽に同じようなこともできるようになりつつありますね: もちろんActive Recordのようにサーバーサイドでないと解決できないことは依然としてありますが」「従来だとサーバーサイドでAPIを作らないといけなかったのが、GraphQLによってフロントエンジニアがサーバーサイドエンジニアを煩わせずにやれる部分が増えてきたように思います」「それが主戦場がクライアント側に移ったということですね」

「逆にサーバーサイドでないとできないことは何かというと、データの永続化や、複雑なドメインロジックの取り扱いのような、ブラックボックスの中をいじる部分」「たしかに」

「フロントエンド側でやれることが増えてきた分、サーバーサイドエンジニアは今後そうしたブラックボックスの中というか下のレイヤに注力する方が今後生き残りやすいのかもしれないとちょっと思いました」「考えさせらる…」

「onkさんの記事、設計方法の変遷を見渡すことができて素晴らしい👍」「ありがたいです🙏」

スライド『「フロントエンド領域」を再定義する』もチェックしておきたい

「そういえばつい最近mizchiさんもフロントエンドエンジニアの職務範囲についてスライドを発表していましたね」「あ、これですか↓」「そうそう、ここで説明されているフロントエンドの変遷もいろいろ納得がいきました👍」

「従来のモノリシックなフレームワークでは↓、サーバーサイドエンジニアがJSやHTMLまでカバーしていて、マークアップエンジニアがいなければ上から下まで全部やっていた」「とても見やすい図ですね」「Railsの推奨構成は基本的にこれ」

「やがてフロントエンドフレームワークが使われるようになると↓、サーバーは永続化とAPIに集中するようになってくるけど、SSR(Server Side Rendering)をやろうとするとサーバー側かフロントエンド側のどちらかが相手側に越境しないといけなくなってつらくなってきた」「ふむふむ」

参考: 【動画付き】Next.js の Server Side Rendering (SSR) を理解する。create-react-app と比べてみた。 - Qiita

「スライドではそこから更に進んで以下のようにフロントエンドフレームワークがSSRを扱う(つまりサーバー側のレンダリングをフロントエンドで行う)ようになっている↓: そうなるとサーバー側はますますバックエンドAPIに徹するようになってきて、基本的な画面もサーバー側では扱わなくなってきたりする」「最近のフル装備フロントエンドはこんな構成なのか」「フロントエンドの職務範囲が広がってきているんですね」

「先のことはわかりませんが、将来こんなふうにフロントエンドがフルスタックになったらどうなるんだろう↓」「ここまでくると何だか逆方向に振り切って最初のレガシー図に近い形に戻ってませんか?」「結局そういうことですよね😆」「Twitterでも指摘してた人を見ました」「歴史は繰り返しつつあるのかも」

「上のようにフルスタックのフロントエンド構成になったら、APIのインターフェイス部分についてもフロントエンドが書くことになりますが、使うのはフロントエンド側なのでそこは自分たちが使いやすいように書けばいいと思います」「ただ、そのAPIの内部で使うドメインロジックについてはサーバーサイドのブラックボックスの中にしか書けないものもまだまだあるので、フロントエンドと通信するAPIのさらに裏にそのための「内部API」的なものが引き続き必要でしょうね」「APIのAPIという感じですね」「その部分がおそらくonkさんの記事で言う『本当に複雑なもの』に相当するかもしれないと最近思っています」

「少なくともサーバーサイドの仕事がいきなりなくなることはないんじゃないかなと想像してます」「現実にはサーバーサイドエンジニアも多少なりともフロントエンドを触ることになるでしょうね」「ただRailsの位置づけがバックエンドAPI中心になることが増えているのは感じています」「少々極端な想像ですが、たとえば今後Railsサーバーがフロントエンドと従来のような直接のWeb API通信をしなくなって、gRPCのようなものを介して通信する一種のドメインロジック用内部サーバーのようなものになる、という形も一応考えられますね(なお今のRailsはそういうものをまったく目指していないと思います)」

参考: サービス間通信のための新技術「gRPC」入門 | さくらのナレッジ

「Railsフレームワークの立ち位置は、ある意味でCSSフレームワークにおけるBootstrapと少し似ているかもしれませんね: プロダクトが成功してエンジニアリソースを多く割り当てられるようになり、継続開発期間が長くなるに連れて、だんだんBootstrapを必要としなくなったり好みが分かれたりすることもあるけど、プロトタイプを最初に作るときはやっぱりBootstrapが早いし便利なので今も使われ続けている、それと似たような立ち位置」「なるほど」

twbs/bootstrap - GitHub


つっつき後に、「週刊気になったITニュース」↓でも「フロントエンド領域を再定義する」スライドが取り上げられていたことに気づきました。

参考: 週刊気になったITニュース(2020/11/15号) - masa寿司の日記

Railsビューのパーシャルがパフォーマンスに与える影響(Ruby Weeklyより)


つっつきボイス:「ビューのパーシャルがパフォーマンスに与える影響の記事は周期的に見かけますね: パーシャルをループで回したりするよりコレクションを使ったりインライン展開したりする方が速いとか↓」「定番のお題ですね」


同記事より

「Railsのビュー周りは折に触れてパフォーマンスが改善されているので昔と違う部分があるかも」「定番の話ではありますがパフォーマンスへの影響が大きい部分なので、最新のRailsの実装で測定結果がどう変わっているかを知るうえでこういう記事を定期的にチェックしておくとよいと思います」「たとえばERBの速度が大きく向上したら、従来だと10倍の差だったのが20倍になったりするようなことがあるかもしれませんよね」

Railsから切り出されたgem完全ガイド

切り出されたgemがshimと呼ばれているとは知りませんでした。


つっつきボイス:「Railsからいろんな機能がgemに切り出されている」「こういう歴史的な記事はありがたい」「Rails 4.2で切り出されたgemが割と多いのか」「そうしておかないとRailsがいくらでも大きくなってしまいますね」

「shimといえばrbenvの設定にshimsディレクトリがありますね」「関連記事にもrbenvのことが書かれてる↓」「ホントだ」

参考: What is a shim?. A shim is a small library that… | by Ujjawal Dixit | Medium

RailsのトップFAQ 12件(Hacklinesより)


つっつきボイス:「質問の内容はとても基本的なんですが、頻度の高い質問に答えている記事です」「お〜、何て丁寧な回答!」「マイグレーションのロールバックとか、RubyやRailsのnilemptyblankの違いなどにみっちり答えているのがスゴい👍」「Railsにまだ慣れてない人にとって便利そう」

「と思ったら質問は12件なのに回答は5件しかない…?」「あれれ、ホントだ」「今後追加するのかな?」


後で気が付きましたが、元記事の末尾からリンクされていた以下の記事に12件のQ&Aがすべて載っていました。少々わかりにくいですね。


前編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201111後編)RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

週刊Railsウォッチ(20201117後編)Rubyのパターンマッチングが3.0で本採用に、AWS Lambdaサイズを縮小する、AppleのM1チップほか

$
0
0

こんにちは、hachi8833です。Googleフォトの変更でちょっとしょんぼりしてます。


つっつきボイス:「いつかは来るかもしれないという気はしていましたが」「さすがにここでハシゴを外しにくるのはキツいかな」「月250円で100GBならリーズナブルかなとも思いますけどね」「やはり容量無限のストレージというものはなかったか」

「一応GoogleのPixelからの利用なら引き続き無料かつ無制限になるようですね」「Googleのスマホを買えば使えるという方針は理解できる」「最初からそういう制度にしておけばよかったかもしれませんね」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓Ruby

⚓ パターンマッチングがexperimentalから本採用へ(Ruby Weeklyより)

# NEWS.md#L51
-* Find pattern is added. [[Feature #16828]]
+* Pattern matching is no longer experimental. [[Feature #17260]]
+
+* One-line pattern matching now uses `=>` instead of `in`.  [EXPERIMENTAL]
+ [[Feature #17260]]
+
+   ```ruby
+   # version 3.0
+   {a: 0, b: 1} => {a:}
+   p a # => 0
+    
+   # version 2.7
+   {a: 0, b: 1} in {a:}
+   p a # => 0
+   ```
+
+* Find pattern is added.  [EXPERIMENTAL]
+ [[Feature #16828]]

つっつきボイス:「おぉ、ついにRubyのパターンマッチングからExperimentalが外れた🎉」「ワンラインパターンマッチングなどの一部のパターンマッチング機能はまだexperimentalなのか」「これで3.0になったらパターンマッチングを使ってもよくなりますね👍


なお、つっつきの後でパターンマッチングの公式ドキュメントが欲しいというissueを見かけました。

参考: Misc #17329: Doc for pattern match? - Ruby master - Ruby Issue Tracking System

⚓ Rubyのrefinementにいいユースケースがひとつある(Hacklinesより)


つっつきボイス:「one good (rare) use caseと書かれているのがちょっと面白い」「いいユースケースはひとつならあると」「refinementのいい使い方はもっとありそうな気がしますけど」「記事の末尾にも『いいユースケースが他にもあったらぜひ教えて下さい』とありますね」「普段のビジネスアプリケーション開発だとあまり使わないかもしれませんが、ライブラリの挙動を安全に上書きするなど知っておくと有効なユースケースはそれなりにあると思います」

「この記事のconversion wrapperはrefinementでないとできないと書かれてますね」「Kernelをrefineしてる…」

# 同記事より
module TimestampConversionRefinement
  refine Kernel do
    def Timestamp(value)
      return value if value.is_a?(Timestamp)
      Timestamp.new(value)
    end
  end
end

class ElegantBusiness
  using TimestampConversionRefinement

  def self.business; Timestamp(1); end
  def business; Timestamp(1); end
end

# ElegantBusiness.Timestamp(1) # NoMethodError: undefined method `Timestamp' for ElegantBusiness:Class
# ElegantBusiness.send(:Timestamp, 1) # NoMethodError: undefined method `Timestamp' for ElegantBusiness:Class

「Rubyのrefinementは乱用すべきではありませんが、いざというときには便利な機能ですし、有効に使えるところで使う分には良いのではないかと思います」

RubyのRefinement(翻訳: 公式ドキュメントより)

⚓ RubyのQuick tips記事2本

つっつきボイス:「小ネタ集その1は、StringとRegexのどちらを渡しても比較できるようにするにはmatch?ではなくトリプルイコール===にする必要があるそうです」「えぇぇ?言われてみれば😳」「たしかにそのとおりなんですけど、これを意識的に使う機会ってあるんだろうか?」「===で比較しているコードを見て『ああ、これはStringでもRegexでも比較できるのね』とすぐに気付ける気がしない…」「コメントにその旨を書いてあれば使うのはありかなと思いました」

def foo(expected, actual)
  expected === actual # match?だとStringを渡せない
end

「小ネタその2で、fdivというメソッドを初めて知りました」「浮動小数点値を得るために数値をto_fしてから割らなくても、fdivなら一発でできるのね」

1 / 5.to_f  #=> 0.2

1.fdiv(5)  #=> 0.2

fdivは知らなかった」「こういうニッチなメソッドはあまり覚えなさそうですが、こうやって一度でも見かけたことがあれば、後で『Rubyにこんなメソッドがないかな』と探すときに思い出す手がかりになるでしょうね」「たしかに、見たことがないと存在すること自体思い当たらないかも」「自分も明日には忘れそうです😆

「ところで、基本的な演算子をこんな感じでメソッドでも呼び出せるようになっていると、Rubyでよくあるtapを何度もチェインするときやカリー化などで、普通のメソッドと同じように演算子メソッドもチェインできますね」「tapやカリー化といえばkazzさんが詳しいというか好きですね」

Ruby: Object#tap、Object#then を使ってみよう

参考: カリー化 - Wikipedia

⚓クラウド/コンテナ/インフラ/Serverless

⚓ node_modulesを片付けてLambdaのサイズを1/3に(Serverless Statusより)


つっつきボイス:「node_modulesを小さくするのね」「Lambdaのサイズでメモリのフットプリントが変わるから、不要なモジュールを減らせれば相当小さくなりそう」

「なるほど、aws-sdkも削除するのか」「aws-sdkはたしかデフォルトで入るんだったかな」「他のAWSサービスを呼び出すためにLambdaを使う場合なら、aws-sdkがデフォルトで入っていると便利ですね」

aws/aws-sdk-js - GitHub

「その他にTypeScriptのtypeファイルも削除できる」「Lambdaでは不要なので削除できますね」

「そしてこれだけ小さくなった↓」「150MBが46MBになったのは大きい🎉


同記事より

⚓ その他クラウド


つっつきボイス:「Eclipse Theiaを初めて知りました」「Theiaって女性の名前っぽい響きがありますね」「それにしてもEclipseがこんなふうに進化していたとは」

後で調べるとギリシア神話の女神テイアでした↓。

参考: Theiaの意味・使い方・読み方 | Weblio英和辞書

「ブラウザ画面でVSCodeのように編集するのはAWS Cloud9などでもできますけど、これは書いたコードをGoogleのクラウドにそのままデプロイできる、つまり書いたものをちょうどこの図↓のようにGCPのCloud Runで実行できるなどの形で実行環境も統合されているところが強みでしょうね」「それは便利そう!」


cloud.google.comより

参考: AWS Cloud9(Cloud IDE でコードを記述、実行、デバッグ)| AWS
参考: Cloud Run: Container to production in seconds  |  Google Cloud

「Cloud9の編集画面は単にWeb IDEのインスタンスが立てられているようなものなので、そこで書いたものをそのまま実行環境にデプロイできるわけではないんですよ」「なるほど、そういうことでしたか」「おそらくGitHubやGitLabのWeb IDEもCloud9のと実質同じだろうと思います」

「実行環境も統合するのはまったく新しいかというとそうでもなくて、たとえばC4SAというサービス↓はなかなか良かったのですが(現在は終了)、GCPと統合されているのは実用性が高いと思います👍

参考: PaaS「ニフティクラウド C4SA」正式版--Ruby on RailsやMySQLも提供 - CNET Japan

「AWS Lambdaのエディタが現在のものより賢くなればこんな感じになるのかもしれない」「Lambdaにもエディタがあるんですね」「このGCP+Eclipse Theiaほどリッチではありませんが、一応編集環境はあります」

参考: AWS Lambda コンソールエディタを使用した関数の作成 - AWS Lambda

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓ NAT Slipstreaming攻撃


つっつきボイス:「NAT Slipstreamingか」「図を見るのがわかりやすい」「SIPはSession Initiation Protocolなんですね」

参考: Session Initiation Protocol(SIP) - Wikipedia

「攻撃の形態としては以前からあるかなと思います」「たしかにオンラインゲームなどでよく使われる『NAT越え』↓という技術では、どこかのタイミングでポートマッピングして外部との経路を開通させるので、そこを悪用されると乗っ取られる可能性がありますが、そういう攻撃の亜種のようですね」

参考: NAT越え(NATトラバーサル-NAT traversal)|Web会議・テレビ会議システムならLiveOn(ライブオン)

「SIPの5060/5061ポートへのリクエストがブラウザによってブロックされるように変更されて、whatwgもブロック対象に加えたのか」「通常業務でこのポートをインターネット上に公開することはあまりなさそうなのでそれほど大きな問題にはならないかな」「こういう攻撃手法がまだあったとは」

⚓言語/ツール/OS/CPU

⚓ scpコマンド非推奨化について(StatusCode Weeklyより)

昨年に宣言されていたんですね。

参考: OpenSSH 公式による scp 非推奨宣言を受け, scp, sftp, rsync を比較してみた (2020/5/25 rsync の計測結果について注記追加) - 寒月記


つっつきボイス:「たしかscp非推奨化は去年あたりに話題になりましたね」「この記事見るまで知りませんでした😅」「非互換の問題があるためにscpを抜本的に変えられない話などもあったと思います」「今はだいたいsftpプロトコルを使いますね」「レガシーなコマンドによくある問題」

参考: SSH File Transfer Protocol(sftp) - Wikipedia

⚓その他

⚓ AppleのM1チップ

参考: Apple M1 - Wikipedia


つっつきボイス:「AppleのM1といえば、早くもGeekbenchでベンチマークが出ていたのを見かけましたけど、何かのバグかと思うぐらい飛び抜けたスコアを叩き出していましたね」「おぉ〜!」「シングルコアでデスクトップのiMacよりも速いとか、にわかには信じがたいスコア↓」「これはすごい」

参考: Apple M1、3.2GHz動作でCore i9-10910超のシングルコアスコア - iPhone Mania


iphone-mania.jpより

参考: MacBookAir10,1 - Geekbench Browser
参考: Macmini9,1 - Geekbench Browser
参考: MacBookPro17,1 - Geekbench Browser

「M1のベンチマークスコアはすごくても今の段階ではソフトウェアがついてきていませんが、ソフトウェアはそのうち追いついてくるものなのでそこは大丈夫だと思います」

なお、現時点ではHomebrewは大半のformulaが未対応または未チェック、MacPortsはARM 64(M1のアーキテクチャ)に対応したそうです。

参考: macOS 11.0 Big Sur compatibility on Apple Silicon · Issue #7857 · Homebrew/brew
参考: Release MacPorts 2.6.4 · macports/macports-base

「実機のメモリが16GBというのがちょっと少ないかな、それとも足りるかな?」「ただでさえChromeが8GBぐらい使いますから😆」「Chromeはメモリ喰い👄

「M1上でIntelバイナリは動くんでしょうか?」「基本的には多くのソフトウェアがRosetta 2の上で動くようですが(速度はともかく)、CPUアーキテクチャが変わるので、Intel CPU固有の機能を直接使っているとかカリカリにチューニングしたソフトウェア、あとはプロプライエタリなドライバや周辺機器などが動かなくなる可能性は考えられますね」「M1との互換性リストもたしか出始めていたと思います」「そういえば知人が自分で書いたかな漢字変換がM1 Macで動くかどうかを気にしてました」

参考: Rosetta - Wikipedia
参考: Product Compatibility for Apple M1 Silicon Macs | Green Screen Blog

「M1はいろんな意味で興味がありますね」「知り合いにもM1 Macを買う宣言してる人がいるので、人柱報告があがったのを見てから買うかどうか決めようかな」「でも今のPC環境で満ち足りているから急いで買うほどでもないか…」「M1は面白いので追いかけつつ様子見してみよう」

「M1 MacがWeb開発にすぐ使えるかどうかはまだ何とも言えなさそう」「Docker Desktop for Macが動くかどうかがポイントかも」「たしかAppleは公式にハイパーバイザー上でDockerをサポートすると聞いた覚えがありますけど、Dockerのissueにはまだ動かないという報告が上がってるな」「しばらくは人柱になる覚悟が必要そうですね」

「ちなみにAppleのハイパーバイザーのドキュメント↓にはsupported hardwareにApple Siliconで動くと明記されているので、ドキュメントレベルではAppleのハイパーバイザーでM1がサポートされているということになりますね」「ホントだ」

参考: Hypervisor | Apple Developer Documentation

「ハイパーバイザーだけを使うコードは基本的にM1で動くと思いますが、ドキュメントにも書かれているIntelのVT-xやEPTといった独自機能を直接使うコードは当然ながらM1では動きません(M1のCPUに命令がないので)」「なるほど」

参考: Virtualization Technology (VT-X)を有効にするには - Lenovo Support IT

「ところで、xhyve↓のようなピュアなハイパーバイザーならM1にも移植できるかなと思ったけど、EPTをサポートするCPUが必須とREADMEに書かれているのでダメか」「xhyveは最近更新されてないみたいですね」「xhyveはシンプルなので比較的移植しやすいかもしれないと思ったけど、I/Oもあるからドライバ周りも対応しないといけなさそう」

machyve/xhyve - GitHub

追いかけボイス: 「仮想化ソフトウェアのParallelsも対応するぞ宣言はしてるけどソフトはまだなので、しばらくはx86系のOSとかDockerが動くまで時間かかりそうな気配ですね」

参考: Parallels における Mac および Windows の仮想化、Mac の管理、VDI および RDS ソリューション


なお、本日出た以下のDockerブログ記事によると、M1上のDocker DesktopはまだRosetta 2での動作が完全ではなく、Appleの新しいハイパーバイザーフレームワークへの移行や手直しを検討しているようです。


後編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201116前編)6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Hacklines

Hacklines

StatusCode Weekly

statuscode_weekly_banner

Publickey

publickey_banner_captured

Serverless Status

serverless_status_banner

Railsのルーティングを極める(前編)

$
0
0

更新情報

  • 2014/03/03: 最初の版を公開
  • 2020/11/18: Rails 5と6の情報を反映

Railsのルーティングを極める(後編)もご覧ください。


こんにちは、hachi8833です。今回も弊社CTOの馬場さんによる勉強会のスライドを元に記事を書きました。発表当時はRails3だったので、Rails4情報も追加しました。

⚓ Railsのルーティング(routes)を極めよう

2012/03: baba

Railsのルーティングはきわめて自由度が高い分、気を付けないとすぐカオスになってしまいます。Railsのルーティングのコツについて勉強していきましょう。Railsのルーティングはconfig/routes.rbで設定します。

⚓ まずはrails routes

追記(2020/11/18):以前はrake routesコマンドでしたが、Rails 5や6ではrails routesも使えるようになりましたので原則こちらで表記します(rake routesも引き続き使えます)。

ルーティングを書く際にはrails routesなどでルーティングを確認しながら書く癖をつけましょう。ルーティングを作成するだけでなく、Railsのデバッグ時にも有用で、迷ったらとにかくrails routesを実行してもいいくらいです。

rails routesは実行のたびにRails環境を読み込むので、そのままでは遅いので有名です。以下のような方法で高速化しましょう。

  • dev環境でRailsを起動中ならhttp://localhost:3000/rails/infoを参照する (Rails 4以降)
  • pryとpry-rails gemがインストールされているなら、rails consoleを起動しておいてshow-routesを実行
  • spring gemがインストールされているならspring rails routesとすれば2度目以降から高速になる

参考までに、週刊Railsウォッチ20201012ではRailsのルーティング情報を動的にビジュアル表示する方法が紹介されています(graphvizをインストールする必要があります)。

⚓ 改めて、RESTとは

RESTとは、Railsに敷かれているレールの一つである概念で、REpresentational State Transferの頭字語です。RESTの特徴は以下のとおりです。

  • ステートレスであること
  • すべてを「リソース」で表す
  • リソースは名前を持つ

RESTに従っていることをRESTfulと呼んだりします。

⚓ RESTfulなURLの例

  • http://example.com/prefectures
  • http://example.com/users/1

⚓ RESTの反対はRPC

RESTのちょうど反対の概念がRPC(Remote Procedure Call)です。これはクエリ形式などと呼ばれることもあることからわかるように、?の後ろに問い合わせを&でつないで表現します。

⚓ RPC URLの例

  • http://example.com/index.php?action=prefecture_list&amp;id=1
  • http://example.com/PrefectureList.aspx?id=1

Railsではresourcesでルーティングを記述することで自動的にRESTfulなルーティングが生成されます。
以下はusersコントローラとproductsコントローラへのルーティングです。

resources :users
resources :products

その場合、対応するコントローラにもRESTfulなアクションが揃っている必要があります。rails generate scaffoldで生成した場合は自動的にRESTfulになります。

Railsでは、REST形式もRPC形式も両方扱うことができますが、RailsはRESTを「レール」として定めていますので、基本的には統一のためにRESTfulなルーティングの作成を心がけるようにしましょう。

ただし、RESTはあくまでポリシーであり万能ではないので、時にはRPC形式を一部に導入する方が素直に作れることもあります。

⚓ RESTメソッド

RESTとHTTPメソッドにはそれぞれ以下のような関係があります。なお、Rails 4 からはPUT非推奨となり、PATCHが推奨されています。

⚓ RESTメソッド一覧

メソッド 安全 冪等
GET
POST × ×
PATCH/PUT ×
DELETE ×
  • 安全が×になっているのは、危険という意味ではなく、実行すると元のデータが更新されるという意味です。
  • 同様に、安全が◯になっているのは、実行によって更新される心配がないという意味です。
  • 冪等(べきとう: idempotent)は近年よく使われる用語で、「1回実行しても2回以上実行しても結果が変わらない」ことを指します(例: 1人殺しても3人殺しても死刑、は冪等です。1人殺せば犯罪者、1000人殺せば英雄、1億人殺せば神、だと冪等ではありません)。chefやvagrantなどのサーバーデプロイ用DSLではその目的のため冪等性が重視されます。

⚓ RESTfulなメソッドとURLの例

以下の表では、BPSという会社の所在地をGETメソッドとRESTfulなURLで表現した場合の例を示しています。

概念 RESTfulなメソッドとURL
都道府県 GET /prefectures
東京都 GET /prefectures/tokyo
東京都市区町村一覧 GET /prefectures/tokyo/cities
東京都新宿区 GET /prefectures/tokyo/cities/shinjuku
東京都新宿区会社一覧 GET /prefectures/tokyo/cities/shinjuku/companies
東京都新宿区にあるBPSという会社 GET /prefectures/tokyo/cities/shinjuku/companies/bps
BPSという会社 GET /companies/bps

以下の表は、記事・ユーザ・コメントを表現した場合の例です。特に、IDが複数ある場合の表現方法にご注目ください。

概念 RESTfulなメソッドとURL
記事一覧 GET /articles
記事(ID=1)、コメント一覧 GET /articles/1/comments
記事(ID=1)、コメント(ID=1) GET /articles/1/comments/1
ユーザ(ID=1) GET /users/1
ユーザ(ID=1)、パスワード GET /users/1/password

以下の表は、記事・ユーザ・コメントに対して操作を行なう場合の例です。Rails 4以降ではPUT非推奨になり、PATCHが推奨されます。

概念 RESTfulなメソッドとURL
記事を投稿する POST /articles
記事(ID=1)にコメントを投稿する POST /articles/1/comments
記事(ID=1)を更新する PATCH /articles/1
記事(ID=1)のコメント(ID=1)を更新する PATCH /articles/1/comments/1
記事(ID=1)を削除する DELETE /articles/1
ユーザ(ID=1)のパスワードを更新する PATCH /users/1/password
(エラー) POST /users/1/password

⚓ ルーティングを綺麗に書くコツ

⚓ 1. 原則は「RESTに従う」

原則として、RESTに従うようにしましょう。頑張ればresourceですべて書くことができます。ただし原理主義的に何が何でもresourceで書こうとすると、かえって見通しが悪くなることもありますので、ほどほどにしましょう。

  resources :admin_menus, only:[ :index, :update ]
  resources :menus, only:[ :index ]
  resources :deadlines, except:[ :create, :destroy ]

⚓ 2.以下の用語を理解する

リソース
HTTPメソッド(GET/POST/PATCH/PUT/DELETE)で操作する対象となるURLです。
名前付きルート
「パス_path」(ドメイン名より下のパスのみ)または「パス_url」(httpなどから始まるフルパス)という形式でURL形式を指定できます。たとえばContactページが/contactというパスにある場合、contact_pathまたはcontact_urlという名前付きルートで指定できます。名前付きルートは、パスヘルパーやURLヘルパーとも呼ばれます。これにより、コード上でURL構造を意識せずにパスを指定できます。
なお似ているけど違うのは「名前付きスコープ」と「名前付きパラメータ」です。
ネストしたリソース
ネストしたルーティングを記述することで、あるリソースを他のリソースの子にすることができます。

ネストしたリソースなどについて詳しくは、後編で解説します。

⚓ 3. アルファベット順にソートする

これはメンテナンスのしやすさに通じます。

なお、ルーティングはファイルの上から順に有効になりますので、同じものを下に書いても効きません。

⚓ 4. ルーティングファイルの分割を検討する

Railsアプリケーションが成長してルーティングが膨大になったら、config/routes.rbの分割を検討しましょう。

たとえば以下のようにconfig/application.rbに記載することで、config/routes.rbを分割してconfig/routes/以下のroutes_1.rbとroutes_2.rbに置くことができます。

config.paths["config/routes"] << "config/routes/routes_1.rb" 
config.paths["config/routes"] << "config/routes/routes_2.rb" 

関連記事

Railsのルーティングを極める (後編)

Ruby: Symbol#nameでシンボル名に対応するfrozen stringを返す(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby: Symbol#nameでシンボル名に対応するfrozen stringを返す(翻訳)

Ruby 3.0のSymbolクラスに#nameインスタンスメソッドが追加されました(#3514)。

このメソッドは、Symbolクラスのインスタンスである背後の値と同じStringクラスのインスタンスを返します。

  :test.name
=> "test"

返される文字列はfrozenになります。

irb(main):002:0> :laptop.name
=> "laptop"
irb(main):003:0> :laptop.name.object_id
=> 180
irb(main):004:0> :laptop.name.object_id
=> 180
irb(main):005:0> :laptop.name.frozen?
=> true

上の例では、:laptop.nameの呼び出しが異なっていても同じobject_idが返ります。つまり、”laptop”の最初のアロケーションの後でStringクラスの新しいインスタンスがアロケーションされないということです。

⚓ シンボルで#to_sを呼んだ場合との違い

シンボルで#to_sを呼んだ場合、戻り値の文字列はfrozenではありません。

したがって、Symbolの同じインスタンスで#to_sを再度呼び出すと、Stringクラスの新しいインスタンスが作成されます。

irb(main):003:0> :laptop.to_s
=> "laptop"
irb(main):004:0> :laptop.to_s.object_id
=> 200
irb(main):005:0> :laptop.to_s.object_id
=> 220

上の例では、:laptop#to_sを呼ぶたびにStringクラスの異なるインスタンスが生成されていることがわかります。

⚓ このメソッドの使いみちは?

シンボルで#to_sを呼び出すコード片がアプリケーションのホットスポット(頻繁に実行される箇所)に存在していると、Stringクラスのインスタンスが大量に生成されて最終的にGC(ガベージコレクション)が行われる必要が生じ、GCによる負荷が増大します。

シンボルで#nameを呼べば、Stringクラスのfrozenインスタンスを得られます。

その後Symbolクラスの同じインスタンスで#nameを呼び出しても、先に得られたこのfrozen文字列が再利用されます。つまり、GCのオーバーヘッドが生じなくなります。

⚓ これまでの経緯

少し前のことですが、Symbolクラスで#to_sを呼び出すとfrozenのStringインスタンスを返す提案が出されました(#16150)。

そしてRuby 2.7で提案どおりに実装されました(#2437)。

しかしその後、残念ながら後方互換性の問題が生じたために取り消さざるを得ませんでした(#16150のnote-45

関連記事

Ruby: Symbol#to_sはRuby 2.7 previewでfrozen Stringを返したが今は違う(翻訳)

はじめての正規表現とベストプラクティス#2: 正規表現とは何か/ワイルドカードとの違い

$
0
0

更新情報

  • 2018/10/12: 初版公開
  • 2020/11/12: 更新

⚓ 正規表現とは

正規表現は「Regular Expression」を直訳したもので、しばしばRegexpとかRegexと略されます(たまにREとも略されます)。正規表現について詳しく説明するときりがありませんが、さしあたって「言語やツール内で利用できる一種のサブ言語」と考えておけばほぼ間に合います。

前回ご紹介した、正規表現に用いられる特殊な記号をメタ文字(メタキャラクタ)と呼びます。

たとえば、以下は正規表現の例です(Rubular)。

[。、,.]{2,}

これは、『「。」「、」「,」「.」のいずれかが2文字以上続いている』ことを表しています。「。。」のように同じ2文字の連続だけではなく、「。、」のように異なる2文字にもマッチします。

⚓ 補足: 正規表現は理論が先に生まれた

正規表現は、理論が実装よりも先に誕生している点が特徴で(SQLも理論から先にできた言語です)、言語学や形式言語理論などの研究の過程で編み出された表現手段の一種です。当初は、表現に不可欠なごくわずかな記号しかなかったようです。

それが後にコンピュータでの文字列の検索や置き換えの表現方法として取り入れられ、実装や拡張が繰り返されて現在に至っています。

正規表現の背景などについて詳しくはWikipediaや正規表現をチョムスキー言語学まで遡って理解する(翻訳)などに譲ります。

参考: 正規表現 - Wikipedia

⚓ 正規表現機能の使いみち

正規表現は、コンピュータでの検索や置き換えの対象となる文字列を指定するのに使われています。

  • 基本機能
    • 特定の文字パターンにマッチするかどうかを判定する
    • Rubyなら"文字列".match?(/正規表現/)などと書ける
  • 付加機能:
    • 特定の文字パターンにマッチしたものを置き換える
      • Rubyなら"文字列".gsub(/正規表現/, "置き換え文字列")
    • マッチした文字パターンを分解して部分を取り出したり部分置き換えする(今後解説します)

正規表現は、コード内のサブ言語の一種としての使い道の他に、テキストエディタやコマンドラインツールでも検索・置換機能のために内部でしばしば正規表現がライブラリまたは独自実装の形で利用されています。また、ApacheやSafariといった規模の大きいソフトウェアにも正規表現ライブラリが組み込まれています。

正規表現はプログラミングで「時々」とても重要になりますが、サブ言語だけあってほとんどの場合主役ではなく、一部の好き者を除いてガンガン使いまくる人はあまりいません。

現実のプログラミングでは、ある種の問題、具体的には「ある程度以上複雑な文字のマッチングや置き換え」を解決するときに使われることが多く、その場の問題が解決されればむしろ正規表現のことなど忘れていたい開発者の方が多いかもしれません。

正規表現は無理して使うものではありませんが、文字のマッチングや置き換えを扱うときに遅かれ早かれ使うことになります。また、Rubyのメタプログラミングでは正規表現が援用されることがしばしばありますので、プログラミングの幅を広げることができます。

⚓ 補足: 正規表現はプログラミング言語ではない

正規表現は言語の一種ですが、プログラミングで使われるにもかかわらず、正規表現は厳密に言うとプログラミング言語ではありません(上述のSQLもやはりプログラミング言語ではなく、クエリ言語です)。プログラミング言語と呼ばれるためには「チューリング完全」という性質を満たす必要がありますが、正規表現やSQLはこの性質を満たさないので「チューリング不完全」とされています。

なお、条件分岐とジャンプなどで「ループを形成する機能」がある言語はチューリング完全になるはずです。

チューリング不完全かどうかは、言語の良し悪しとは別です。むしろ正規表現やSQLの仕様は、積極的にチューリング完全にならないことを目指しています。これによって無限ループが原理的に発生しなくなり、どんなに時間がかかろうと「いつかは必ず終わる」ことを確信できるからです。

もし正規表現がうっかりチューリング完全になってしまうと、正規表現だけでプログラミングできるようになり、メインのプログラミング言語の役割を侵食してしまいかねません。

参考: 本の虫: うっかりチューリング完全になっちゃったもの

⚓ 正規表現を使えるCLIツールの例

正規表現の使えるコマンドラインツールとしてはgrepが有名ですが、他にも文字列置き換えに特化したsedなど、ここに書ききれないほどさまざまなコマンドがあります。

なお、私自身はripgrepというgrepよりも高機能/高速なツールを使っています。

BurntSushi/ripgrep - GitHub

pecoというインタラクティブなフィルタツールも非常に便利なのでよく使いますが、これにも正規表現モードがあります。

peco/peco - GitHub

⚓ 正規表現は「ライブラリ」で実現される

正規表現の機能は、プログラミング言語やツールのコアに組み込まれることはあまりなく、多くの場合その言語やツールのライブラリ(エンジンとも呼ばれます)という差し替え可能な形で導入されます。

したがって、異なる言語であってもライブラリが同じであれば同じ正規表現が使えますし、逆に同じ言語であっても正規表現ライブラリが変更またはアップデートされれば機能が追加されたり、逆に一部で互換性がなくなることもありえます。

Rubyの場合、Onigmo(鬼雲)というライブラリが使われています。以前はoniguruma(鬼車)というライブラリでした。

正規表現をサポートするエディタの中には、正規表現のライブラリを差し替えることでより強力な正規表現の機能を使えるものもあります(秀丸エディタなど)。

⚓ 正規表現には「方言」がある

正規表現のライブラリや実装が多様であるため、残念ながら正規表現には方言がものすごく多いという問題があります。具体的には、さまざまな言語/ライブラリ/ツールで実装されている正規表現はたいてい独自拡張されています

第1回で説明した正規表現はほぼ確実に共通で使えるのが救いです。

ライブラリやツールごとの互換性については以下のWikipediaページによくまとまっています。

参考: Comparison of regular expression engines - Wikipedia

⚓POSIX系

Linuxコマンド系ではよくPOSIXまたはPOSIX互換という言葉を見かけます。POSIXには「POSIX基本正規表現(BRE)」と「POSIX拡張正規表現(ERE)」があります。

どちらも現代(私)の目から見るとかなり機能が不足しがちで、しかも統一されているとも言えません。私は「POSIX互換」と書いてある正規表現ライブラリを見るとがっかりする方です。

かつPOSIX拡張正規表現の中には[[:name:]]だの[:alpha:]という、Unicodeコンシャスかどうかも疑わしい独特のショートハンド方言があったりします。[: :]だったり[[: :]]だったりと書式もばらつきがちです。

参考: POSIX Basic and Extended Regular Expressions
参考: POSIX Extended Regular Expression Syntax - 1.50.0

ここは私個人の意見ですが、少なくとも正規表現を複数のライブラリにまたがって使う場合、[[:name:]][:alpha:]といった形式のショートハンドは避ける方が無難だと思います(ショートハンドについては今後の記事でご紹介します)。私はこれらを使いません。
少なくとも、[[:alpha:]]がその名に反して漢字やひらがなやカタカナにまでマッチしてしまうことは覚えておくとよいと思います(Rubular)。

また、「POSIX基本表現だけ使えばどこででも使える」という最大公約数的な考えも現実的ではないと思います。現実に複数ライブラリで共有したいのはもっと高度な機能であることが多いので(今後扱います)、POSIX基本表現を安易に最大公約数と考えない方がよいと思っています。

⚓ PCRE系

正規表現ライブラリの中でもうひとつメジャーなのはPCREと呼ばれるPerl 5互換の正規表現ライブラリです。PCREで拡張されている正規表現はかなり強力なのですが、それでも(私には)少し足りない機能があります。

また、Perl 5はPCREそのものを用いていますが、PHPやPythonの正規表現やRubyのOnigmoなどはPCREにかなり近いながらも独自実装であり、実際にはPCREよりさらに機能が足りない部分や独自の拡張がありますので、PCREと完全互換ではありませんし、お互い同士にも互換性のない部分があります。機能が不足する場合、開発者がより強力な正規表現ライブラリを導入することもあります。

個人的には.NET Frameworkの正規表現ライブラリが総合的に見て最も強力だと思います。ドキュメントも充実しています。

通常の開発やエディタでの検索置換であれば方言を意識する必要はほとんどありませんが、正規表現を複数の言語やプラットフォームで共有する場合は、方言やライブラリ(種類やバージョン)を意識しなければならなくなります

⚓ 正規表現の用語はあまり統一されてない

正規表現ライブラリのドキュメントで使われている用語は、ライブラリ間では統一されているとは言い難いものがあります。歴史を考えると仕方がないとも言えますが、ライブラリごとにそこそこ違っていたりするので、機能を探しているときに迷わせてくれます。その点を頭の隅に置いておきましょう。

⚓ メタ文字を含まない文字列も正規表現の一種

基本的なことですが、厳密に言うと?+などのメタ文字を含まない単なる文字列も正規表現の一種です。言い換えると、あらゆる文字列は正規表現の部分集合とみなせます。したがって、メタ文字を含まない正規表現も「ありです」(Rubular)。

ついでながら、どこからどこまでが正規表現かを表す区切り方は言語やツールの実装によって異なります。Rubyの場合は/ /で囲むことで正規表現を表せます(他の記号を使うこともできる)が、grepなどでは" "で囲んで指定することもあります。エディタの検索窓では区切り文字が不要な場合がほとんどです。

⚓参考: 正規表現にも「AND」が隠れている

ここは今すぐ理解しなくても構いません。

前回ご紹介した正規表現の|というメタ文字は、論理演算で言う「OR」に相当します(否定については今後扱います)。

「ORがあるならANDだってあるよね?」という気持ちになるのは当然です。しかし正規表現のメタ文字をいくら探してもそんなものはありません

実は正規表現の場合、「文字を並べて書くこと自体がANDを表す」のです。言い換えると、ANDは文字と文字とのつながりに隠れています。ANDに相当するメタ文字がないのはそのせいです。

たとえば/AB/という正規表現があるとします。これは次の意味になります。

  • Aという文字がある
  • かつ
  • その後ろにBという文字がある

何だかあまりに当たり前な主張ですが、学問としての正規表現はミニマムをよしとするところがあって、「どんな文字列でも抽象的に表せる最小の表現方法」を追求した結果なのだと想像しています。

ANDの関係は、文字とメタ文字の間、メタ文字とメタ文字の間であっても同じように成立します。

「だからどうした?」と言われればそれまでです。このことを知ったからといって正規表現の書き方がすぐに上達するわけでもありません。しかし正規表現が複雑になり始めたときに、これが助けになることがきっとあると思いますので、何となく頭の隅にでも置いておいてください。

⚓ ワイルドカードと正規表現の違い

⚓ ワイルドカードとは

WordやExcelなどの検索置換機能、OSのファイル検索機能、シェル(bashやDOS窓など)のファイル名展開機能、一部のWebサイト内検索機能などで使われる?*という特殊な記号をワイルドカード(wildcard)と呼びます。元々はトランプのジョーカーのような任意のカードになれる特殊なカードを指す言葉です。

*
任意の文字列を表すワイルドカード
?
任意の1文字を表すワイルドカード

たとえば*.txtとすると「.txtで終わるすべてのファイル名」を表現できます。

記号が2種類なので、一般ユーザーにわかりやすいのが特徴です。WordやExcelでは、ワイルドカードの機能が少し独自拡張されており、正規表現に少し似たことができます。

#「の」の連続を検出するWordのワイルドカード
[!。の]{1,}の[!。の]{1,}の[!。の]{1,}の[!。の]   

参考: ワイルドカード (情報処理) - Wikipedia
参考: 【まとめ】ワイルドカード(正規表現)に関する記事一覧 | みんなのワードマクロ

⚓ ワイルドカードは正規表現ではない

しかし、ワイルドカードは正規表現とは異なるものです。

  • ワイルドカードは表現力が乏しい
  • ワイルドカードの記号は正規表現の記号と意味が異なる

ワイルドカードと正規表現は見た目の印象がよく似ており、Wordの拡張ワイルドカード機能は一見正規表現のように見えるので、紛らわしいのですが、少なくとも両者の?*という記号は以下の点が決定的に異なります。

ワイルドカードの?
「1文字そのものの代わりに置く」(new?など)
正規表現の?
直前の1文字を後ろから修飾する」(/news?/など)

ワイルドカードの*
「任意の長さの文字そのものの代わりに置く」(*.xlsなど)
正規表現の*
直前の1文字を後ろから修飾し、その文字がゼロ文字以上であることを表す」(/./*\.xls/など)

まとめると、?*という記号は、ワイルドカードでは「文字の代替」、正規表現では「直前の文字を修飾する量指定子」となります。

⚓ 第1回で*を紹介しなかった理由

*
直前の1文字の0回以上の繰り返しを表すメタ文字

正規表現を使ったことがある方なら誰もが知っている*(=直前の文字のゼロ回以上の繰り返し)というメタ文字ですが、第1回ではあえて含めませんでした。

取り上げなかった理由は、ワイルドカードの*の考え方に毒されたと思われる.*という正規表現が安易に用いられることが多いためです。

正規表現の書き方は一種類ではないので「機能すればよい」と割り切ることもできますが、.*.+といった正規表現はしばしば曖昧さにつながり、不要なマッチを呼び込みやすくなると個人的に考えています。最長一致や最短一致も意識しなければなりません。

本音を言えば、.+すら基本には含めたくなかったぐらいです。

もちろん.*.+がどうしても必要になることもありますが、その前に{N, M}などのより限定的な量指定子が使えないか、.のような範囲の広すぎるメタキャラクタではなく[]などでもっと限定できないかを検討するようにしています(これらについては今後の記事で解説します)。

これまで多くの正規表現を書いてきましたが、幸いにして*.*を使う機会は(使い捨ての正規表現を除けば)一度もなく、+.+もごくたまに使う程度です。


関連記事

正規表現の先読み・後読み(look ahead、look behind)を活用しよう

Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック

家の鍵を持ち歩きたくない!オートロックもスマホで解錠?スマートロック奮闘記

$
0
0

こんにちは。ウイングドアの田上です❗

みなさん自宅の鍵ってわずらわしくないですか❓
鍵を開ける時にカバンの隅から取り出さないといけなかったり、
取り出しやすいところに入れていたら何かの弾みで無くしてしまったり……😢
ちょっとしたことですが自分にとっては日常のストレスとなってしまっていました💦

常に目の届くところにあるスマホが代わりになったらいいのにな〜と考えていた時に、
スマートロックというものがあることを知りました❗

#morimorihoge注:

※本記事の内容は賃貸物件であれば大家との契約、所有物件の場合でもマンション管理規約等に抵触する可能性があります。
恐らく通常は問題ない範囲のhackかと思いますが、参考に当たっては自己責任でお願いします。

⚓ 「セサミ スマートロック」を導入してみた

みなさんスマートロックをご存知でしょうか❓
ドアの鍵をスマホ操作で解錠・施錠できるようになるというものです。
そう、スマホを自宅の鍵にすることができるのです❗

早速「セサミ スマートロック」を購入し、
ドアノブに取り付けると、専用のSESAMIのアプリからドアの解錠ができるようになりました。

とっても簡単🎉🎉

⚓ SwitchBotでオートロックと立ち向かう……⁉

セサミ スマートロックでドア鍵が開けられるようになり、万事オッケー❗と思っていたのですが、
最近引越しをしまして、別の問題が現れてしまいました……❗

そう、今は大体の賃貸物件には1階のエントランスにオートロックがついているのです😫

今回引越しした物件の場合、受話器を持ち上げないといけないタイプだったので、
鍵を用いずにオートロックを開ける際には以下の動作が必要でした。

  1. 部屋番号を入力後、呼び出しボタンを押す
  2. 部屋に設置してあるインターホンから呼び鈴が鳴るので受話器を取り上げる
  3. 解錠ボタンを押す

くぅ〜、これは流石に回す動作に長けた「セサミ スマートロック」ではできない……🥺
このままでは自宅玄関のドアの解錠が出来ても、エントランスで結局物理鍵を要求されてしまう❗

しかし、諦めずいろいろとネットの海を彷徨っていると、
SwitchBot」というものを使って同じ悩みを解決されている記事が見つかりました……❗

SwitchBotの仕組みは単純で、
アプリの画面からON / OFFを押すことでSwitchBotの突起が出たり戻ったりします。

SwitchBotのアプリから「シーン」を設定することで、
スケジュール設定を行ったり、複数のSwitchBotのアクションを組み合わせることができます。

また、SwitchBot ハブプラスを自宅のインターネットに接続することで自宅の外からでもSwitchBotの操作が可能になります。

つまりSwitchBotを組み合わせれば受話器を持ち上げて解錠ボタンを押すことができる……❗❓
すぐに「SwitchBot」とインターネット越しに遠隔操作するために必要な「SwitchBot ハブプラス」をAmazonで注文しました❗(次の日には到着しました。さすがAmazon)

⚓ Case1 受話器を予め外しておく(失敗)

前述しましたが、鍵を用いずにオートロックを開ける際には以下の動作が必要です。

  1. エントランスで部屋番号を入力後、呼び出しボタンを押す
  2. 部屋に設置してあるインターホンから呼び鈴が鳴るので受話器を取り上げる
  3. 解錠ボタンを押す

この3ステップをクリアすることで、1階の自動ドアが開きます。

SwitchBotを使用して操作出来そうなのは

  1. 部屋に設置してあるインターホンから呼び鈴が鳴るので受話器を取り上げる
  2. 解錠ボタンを押す

なのでまずは受話器を持ち上げる部分を考えました。

受話器には引っかける際に押されるボタンがついており、ここが押されることで受話器の「とる」「かける」を判別しているようです。
なので、最初に

「受話器のボタンを押しっぱなしにしておけば受話器を置いた状態を再現出来るので、あとはインターホン鳴ったタイミングで押しているボタンを離すように調整すればよいのでは🤔

と考えました。

さっそくダイソーで材料を揃えて設置してみることに。

うむむ、いい感じだ。(そうか?🤔
横に受話器をかけておいて1階からインターホンを押してみると

あれ……❓インターホンの音が鳴らない……❓

どうやら受話器のマイク部分とスピーカー部分を使って音を出しているのか、
受話器をちゃんとかけた状態でなければインターホンの音が鳴らないらしい。
これでは普通の来客の際に対応できない……😭

⚓ Case2 受話器を引っ張って持ち上げる + 微調整を力技で頑張る(成功)

さて、なぜCase1で遠回りな方法を取ったのか。
掛かった状態の受話器を上から引っ張れば良いのではないか❓と思うかもしれません。

ただ、この受話器というのはなかなか曲者で単純に上方向に引っ張るだけでは受話器を取れない構造になっています。
手で受話器を取る際にはなかなか意識することはありませんが、
受話器を持ち上げるには、受話器を少し上にあげ、手前に引くようにしないと引っかかってしまうのです。

なので、単純に上から紐で引っ張ろうとしてもこのせいでうまく受話器を取れません。
また、受話器を置き直す際にも手前から押し込むように掛けないと受話器がうまく置けません。

人間が受話器を持ち上げて戻すだけでどれだけ複雑な動作を行っているかが身に染みてわかります(笑)

できるだけシンプルにしたいなという思いから微調整の必要な改修は避けたかったのですが、
参考記事でもオートロックの機種ごとに

  • 「受話器のひっかけ部分に厚紙を挟んで調整」
  • 「バンドで受話器の下部分を固定」

など個別の調整をされていたので、結局自分も
ひっかけ部分にツノを付けることで引っ張ると斜めに滑らせる方法を取りました🤗
(調整がかなり難しく苦戦しました……😫

が、これでSwitchBotをオンにして引っ張ると受話器を持ち上げることができるようになりました❗
SwitchBotをオフにすると受話器を置き直すこともできます。

また、受話器はフックに掛かった紐を外すことで、
普通に持ち上げることもできるようになっているので、
宅配や来客など普通に応対するパターンでも対応が行えます❗(ここも割と力技……🤗

あとは簡単。SwitchBotで解錠ボタンを押すだけです。
インターホンの隣に白い箱を増設し、そこにSwitchBotを設置しました。

これで全ての準備ができました❗

試しに動かしてみる

おおーーーーーーーーーー🎉🎉🎉🎉🎉🎉🎉🎉🎉
これでオートロックのある物件でもスマホだけを持って外出することができるようになりました😭😭😭

まとめ

以上、鍵を持ち歩かないですむようにするための自分の奮闘記でした(笑)

セサミ スマートロックとSwitchBot ハブプラスはどちらとも

などのサービスとも連携できるため、
たまに「Hey Siri!ドアを開けて」と呼びかけてオートロックを開けてもらっています💪

ちなみに、SwitchBot ハブプラスは赤外線を学習させることも出来るので、手元のスマホをエアコンやテレビのリモコンにすることもできます。(リモコンをよく見失うので地味に便利です)

最初は念のため物理鍵も持ち歩いてましたが
動作が安定してきたのでスマホだけ持って外出をしています❗

鍵を持ち歩きたくない自分のような方は参考にしてみてください。
ご精読ありがとうございました❗


株式会社ウイングドアでは、Ruby on RailsやPHPを活用したwebサービス、webサイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。


Railsのルーティングを極める (後編)

$
0
0

更新情報

  • 2014/03/03: 初版公開
  • 2020/11/20: Rails 6で確認および更新

こんにちは、hachi8833です。「Railsのルーティングを極める」の後編です。今回はRails 4.0.3 + Ruby 2.1.1の環境で動作確認しています。

⚓ Railsのルーティング(routes)を極める

2012/03(baba

⚓ resourcesとネスト

Railsのルーティング記法の基本は、複数形のresourcesメソッドと単数形のresourceメソッドです。また、Railsのルーティングにはネストを含む多くのオプションがあり、自由度が飛躍的に高まっています。

以下の2つのルーティングは、ネストしていないシンプルなresourcesルーティングです。prefecturesとarticlesは、いずれもコントローラに合わせて複数形で書く点にご注意ください。

resources :prefectures
resources :articles

rails routesしてみると、prefecturesとarticlesそれぞれについてRESTfulかつ標準的なアクション(indexcreateneweditshowupdatedestroy)を網羅したなルーティングが一気に生成されています。

nonnested

ところで、せっかくなのでRails 4.0以降で使えるルーティング表示機能でも見てみましょう。development環境でRailsを起動して、ブラウザでhttp://localhost:3000/rails/info/routesを開くと以下のように表示されます。

名前付きルートもHelper列にわかりやすく表示され、[Path]と[Url]をクリックすれば名前付きルートの*_path*_urlを切り替えて表示するという細かい芸もやってくれます。

nonnested_4.0

参考までに、最近のRailsでは以下のように表示が洗練されています(6.0.3.4で確認)。

なお名前付きルートのうち、*_pathはドメイン名から下のパス、*_urlはhttp://などから始まるフルパスであることは前編でも説明いたしました。生成された名前付きルートをrails consoleで確認するには、app.に続けて名前付きルートを入力してみます。

namedroutes

「複数形にはidはなく、単数形にはidがある」と覚えておくとよいでしょう。

このようにコードで名前付きルートを使うことで、生のURLをコードに書かずに済みます。

名前付きルートはカスタムで指定することもできますが、なるべくこのように標準的なものをRailsに生成させる方が楽ですし混乱せずに済みます。

では、このうちprefecturesの下でcitiesとcompaniesをネストさせてみましょう。

resources :prefectures do
  resources :cities do
    resources :companies
  end
end

このときのルーティングテーブルは以下のようになります。

nested

見てのとおり、prefecturesのルーティングに加え、prefectures/cities、そしてprefectures/city/companiesという階層が追加されました。いずれも複数形の「resources」を指定しているので、prefectures, city, companyにはidがあります。

このときの名前付きルートは次のようになります。idの部分には適当な数字を入れてあります。

nestedroutes2

⚓ 単数リソース

上では複数形のresourcesを使用してid付きのルーティングを生成しましたが、単純なページへのルーティングのようにidが不要な場合は単数形のresourceを指定することができます。

仮にprefectureでidが不要だとすると、以下のように単数形のresourceを指定し、prefectureも単数で書きます。

resource :prefecture

このときのルーティングは以下のようになります。

resource

見てのとおり、Pathにid:が含まれなくなり、indexアクションもなくなりました。

なお、この記述「resource」も「prefecture」も単数ですが、これによって指定されるコントローラは「prefectures」と複数形になっていることにご注意ください。御存知のとおり、Railsではコントローラ名を複数形で書くことになっています。

名前付きルートを確認します。なお、無効なはずのidをわざと付けてみると、妙なパスが生成されました。

singleresource

⚓ 複数リソースと単数リソースのネスト

今度は複数リソースと単数リソースの組み合わせの例を示します。ユーザーは複数いるのが普通なのでidを指定しますが、ユーザーごとのパスワードは1つしかないのが普通なので、パスワードではidを指定しない、という状況です。

この場合以下のようなルーティングが考えられます。外側のusersは複数リソース、内側のpasswordは単数リソースです

resources :users do
  resource :password
end

この場合は以下のルーティングが生成されます。

combinednest

期待どおり、userにはidがあり、passwordにはidがありません。

なお、user_pathのようにそのリソースがパスの最後尾にある場合のidは「/:id」と表記されていますが、user_password_pathのようにuserがパスの途中にある場合では「/:users_id/」と表記されています。どちらもidです。

nestedsingle

⚓ resourceベースでないルーティングの書き方

resourceベースでない、HTTPメソッドを指定したルーティングも可能です。その方がresourcesで書くよりもルーティングテーブルがシンプルになるのであれば、使う方がよいと思います。

get 'hello1', to: 'pages#hello'
get 'hello2', :controller => 'pages', :action => 'hello2' 
get 'hello3/:id', to: 'pages#hello3'
post 'hello4', to: 'pages#hello4'

⚓ よくない書き方

以前は、GETPOSTPUTUPDATEDELETEのHTTPメソッドすべてにマッチさせたいときはmatchを使ったのだそうですが、ワイルドカードはセキュリティ上の隙になる可能性があるので、Rails 4からvia:オプションなしでのmatch指定は禁止されています。

match :hello5, to: 'pages#hello5' #禁止 (エラーが表示される)
match :hello5, to: 'pages#hello5', via: [:get, :post] #許される

さらに、かつては以下のように書くことができたのだそうです。

match ':controller(/:action(/:id))(.:format)'

こうすると「コントローラ」「アクション」「id」が実在してさえいればupdateやdestory`など何にでもマッチしてしまうという、楽ちんかつ風通しの良すぎるルーティングになります。このヒューヒューの全通しルーティングは当時から危険視されていたらしく、チュートリアルや実験用以外で使うべきでないとされていたようですが、現在は完全に禁止されています。

⚓ namespace

Railsのルーティングでは以下のようにnamespaceを指定してパスをグループ化することができます。これを使用して、たとえば管理用ページ(admin)のパスや置き場所を仕切ることができます。

namespace :page do
  get :privacy_policy
  get :company_information
  get :term_of_use
  get :businessdeal
end

namespaces1

上のように、「名前付きルート」「パス」「コントローラ#アクション」にpageが追加されました。

上は素朴なgetメソッドルーティングでしたが、resourcesルーティングに名前空間を与えることもできます。

namespace :admin do
  resources :users
end

namespaces2

同じく、「名前付きルート」「パス」「コントローラ#アクション」にadminが追加されました。

⚓ :moduleによる名前空間

:moduleオプションを使用してresourcesに名前空間を与えることもできますが、これは上と少し動作が異なります。

resources :users, module: :admin

#以下も同等
scope module: :admin do
  resources :users
end

namespace3

こちらは「コントローラ#アクション」にしかadmin名前空間が追加されていません。ここからわかるように、パスには表したくないが別のディレクトリにまとめたいコントローラがある場合に利用できます。

⚓ collectionとmember

既に見たように、resourcesを使用すれば主要な7つのルーティングが自動的に追加されますが、 それ以外のルーティングをそのリソースに追加したい場合はmemberまたはcollectionを使います。

さっきの「複数形はidなし、単数形はidあり」と同じ考え方で、「collection(集合)はidなし、member(個別)はidあり」と覚えましょう。以下のルーティングを例に取ります。

resources :books do
  collection do
    post :search
    post :remove_multi
  end
  member do
    get :thumbnail
    get :sample_file
  end
end

これをルーティングテーブルにすると以下のようになります。

collectionmember

見てのとおり、いつもの7つのルーティングに加えて4つのルーティングが追加されています。そしてcollectionで指定した2つにはidはなく、memberで指定した2つにはidがあります。

なお、この場合の名前付きルートはbooks_search_pathとかではなくsearch_books_pathのように上位のリソースが後ろに置かれていることにご注意ください。一応名前付きルートも確認してみましょう。

collectmember

以下のような簡略版表記も使用できます。

resources :books do
  post :search, on: :collection
  get :thumbnail, on: :member
end

ここで1つ注意があります。以下のようにcollectionmemberも指定せずに書いた場合はデフォルトで「member扱い」となります。

resources :books do
  post :search
  post :remove_multi
end

上の2つのリソースのルーティングテーブルを見てみると、確かにidが含まれており、member扱いされていることがわかります。

nocollectmember

⚓ root

今更ですが、rootへのルーティングの書き方は以下のとおりです。これだけ他の書き方と比べて少し浮いている感じですね。

root to: 'page#top'

⚓ ルーティングのオプション

最後に、ルーティングでよく使われるオプションを紹介します。

⚓ only:except:

resourcesonly:またはexcept:オプションを使用することで、主要な7つのアクション(index, show, new, create, edit, update, destroy)を限定することができます。

# indexとshowアクションだけ使う場合
resources :prefectures, only: [:index, :show]
# destory アクション以外を使う場合
resources :prefectures, except: :destroy 

updatedestroyのような破壊的なアクションは事前にルーティングレベルで塞いでおきましょう。

⚓ リソース名の変更

asオプションを使用して、リソース名を変更することができます。

get 'home', controller: :users, as: 'user_root'

asoption

⚓ httpsの指定

以下のようにprotocol: httpsを指定することができます。

scope protocol: 'https://', constrains: {protocol: 'https'} do
  root to: 'page#top'
end

なお、アプリの一部だけをHTTPS化するのは手間がかかる上にセキュリティ上の懸念も残るので、アプリ全体をHTTPS化することをおすすめします。

⚓ idを拡張

たとえば、以下のようにidの制約を変更してアルファベットのidを使用することができます。

resources :prefectures, id: '/^[a-z]+$/'

⚓ 最後に

Railsには他にも強力なルーティングのオプションがたくさんありますが、アドホックなカスタムルーティングを避け、なるべくresoucesresourceonlyexceptで素直かつ統一のとれたルーティングを生成するようにします。

コントローラが数百にのぼる巨大なルーティングをすべてresourcesresourceで書いた例もあります。

ただし、そこでRESTfulにしようと頑張り過ぎないのも大事です。

⚓ 追伸

Rails 6.1では、ルーティングの記述を間違えたときにdid you mean?で推測する機能が加わります。

⚓ 参考

関連記事

Railsのルーティングを極める(前編)

Rails: ルーティングを動的にビジュアル表示する方法

$
0
0

以前Railsウォッチ20201012でご紹介した内容を自分用に別記事にしました。

RailsのルーティングをFSM(有限状態機械)の形式でHTMLファイルに出力し、動的にルーティングをシミュレートできる機能です。

参考: 有限オートマトン - Wikipedia

⚓ Railsのルーティングビジュアライザ

⚓ 必要なもの

  • Railsローカル実行環境
  • graphviz

なお、2012年の56fee39でJourneyがAction Dispatchに統合された時点で既にビジュアライザのコードが入っているので、もっと前からこの機能があったようです。少なくともRails 4以上で使えるはずです。

⚓ 利用法

  • 自分のローカル環境にgraphvizをインストールしておきます。MacでHomebrewを使っている場合はbrew install graphvizでインストールできます。
  • Railsプロジェクトのルートディレクトリで以下を実行し、出力されたout.htmlをブラウザで開きます。

$ bin/rails r 'File.binwrite "out.html", <アプリ名>::Application.routes.router.visualizer'

なお<アプリ名>はRailsのアプリ名に置き換えます(アプリ名はconfig/application.rbにある大文字で始まるモジュール名を使います)。

もちろんRailsコンソールでも実行できます。

File.binwrite "out.html", <アプリ名>::Application.routes.router.visualizer

出力されたHTML冒頭のボックスにルーティングを入力して[simulate]を押すと、該当するルーティングがFSM図上でハイライトされます。

⚓ 参考

この機能は、Kaigi on RailsのAaron Pattersonさんによるオープニングキーノートスピーチ『Viewがレンダリングされるまでの技術とその理解』でRailsの秘密機能として紹介されていました(動画は頭出し済み)。

関連記事

Railsのルーティングを極める(前編)

週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか

$
0
0

こんにちは、hachi8833です。今回は短縮版でお送りいたします。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

公式の更新情報から見繕いました。

参考: 6.1.0マイルストーン 6.1.0 Milestone(84%、残り20件)

⚓ strict loading violationの振る舞いを変更できるようになった

元々strict loading violationは、関連付けでオンになっている場合は常にraiseしていた。今回の変更では、railseの代わりにログ出力するオプションをアプリケーションで選べるようになる。なお、この振る舞いはstrong parametersと似ていて、デフォルトではすべての環境でraiseする。アプリケーションは、lazy loadされたすべての関連付けを探しつつ、production環境ではログ出力することもできるようになる。

raiseしないようにするには、config.active_record.action_on_strict_loading_violation:logに設定する。
同PRより大意

# activerecord/lib/active_record/core.rb#273
+     def self.strict_loading_violation!(owner:, association:) # :nodoc:
+       case action_on_strict_loading_violation
+       when :raise
+         message = "`#{association}` called on `#{owner}` is marked for strict_loading and cannot be lazily loaded."
+         raise ActiveRecord::StrictLoadingViolationError.new(message)
+       when :log
+         name = "strict_loading_violation.active_record"
+         ActiveSupport::Notifications.instrument(name, owner: owner, association: association)
+       end
+     end

つっつきボイス:「strict loading violation?」「これはたぶん割と最近入った、ビューの中でSQLを発行できないようにする機能のことでしょうね」「そんな機能が入ってたんですか」「たしかstrict loadingを有効にしておくと、コントローラからActive Recordオブジェクトをビューに渡した後に、たとえばビューの中でpreloadingされていないhas_many関連付け先のデータを取得しようとするとSQLを発行せずにraiseするというものだったと思います」「あ〜、そういうものなんですね」「lazy loadingをraiseして見つけやすくするときなどに使えますね」

後で調べると、以下のプルリクがそれのようです(ウォッチ20200302)。

「今までは有効にすると常にraiseされていたのが、今回のプルリクでは環境に応じてraiseせずにログ出力できるようになったんですね」「今まではできなかったのか」「従来は既存のコードで後からstrict loading violationを有効にすると常にraiseされるのが不便だったけど、これはなかなかいいですね👍

「調べるときはログでlazy loadedな関連付けをチェックして、修正したらraiseするように変更する感じで作業したりできるんですね」「ちょうどSELinuxでpermissive modeを使うのと似てるかも」「私もそれを思い出しました」「audit logは出すけど動作は継続するところなどが似てますね」「permissive modeを使うとちょっとドキドキする😆

参考: Security-Enhanced Linux - Wikipedia

「Railsでも最終的にはraiseするようにすべきですけど、これで検証中はraiseせずにログを出せるようになった」「当初はraiseだけで実装されてたのがちょっとアグレッシブな印象ですね」

⚓ 新機能: マルチプルデータベース向けconnected_to_many

#40370で高粒度コネクションスワップが実装されたので、マルチプルデータベースに接続できる新しいAPIが必要だ。その理由は、たとえば5つのデータベースのうち3つに読み取り用に接続して残りは書き込み用にしたい場合に、このAPIでネストが深くならないようにするためだ。

このAPIがあれば、以下のように書く代わりに

AnimalsRecord.connected_to(role: :reading) do
  MealsRecord.connected_to(role: :reading) do
    Dog.first    # read from animals replica
    Dinner.first # read from meals replica
    Person.first # read from primary writer
  end
end

以下のように書ける。

ActiveRecord::Base.connected_to_many([AnimalsRecord, MealsRecord], role: :reading) do
  Dog.first    # read from animals replica
  Dinner.first # read from meals replica
  Person.first # read from primary writer
end

これは過去の2つのデータベースのネストが深くなる場合に特に便利になるだろう。
同PRより大意

#40370は以前見たときはWIPでした(ウォッチ20201020)。


つっつきボイス:「connected_to_manyは上のサンプルコードがわかりやすい」「connected_to_manyという名前でいいんだろうかという気がしないでもないですけど」「今までは接続先が同じでもネストしないと書けなかったのが、ネストせずに書けるようになった」「このぐらいならネストしててもいいかなとも思いましたけど、機能が増えたのはいいですね👍

⚓ Host Authorizationでもリクエストを除外する機能を追加

リクエストを強制SSLから除外する必要が生じる可能性があるのと同様に、Host Authorizationチェックでもリクエストを除外する必要が生じる可能性がある。アプリケーションでこの柔軟性を増すことで、適合しない可能性のあるリクエストを除外しつつHost Authorizationを有効にできるようになる。たとえば、AWS Classic Load BalancerはHostヘッダーを提供しておらず、Hostヘッダーを送信するように設定することもできない。つまり、このLoad Balanacerのヘルスチェックを使うためにはHost Authoraizationを無効にしなければならなかった。今回の変更によって、アプリケーションはHost Authorizationで必要とされるヘルスチェックリクエストを除外できるようになる。

自分は(ActionDispatch::SSLと同様の方法で)ActionDispatch::HostAuthorizationミドルウェア引数を受け取れるように変更した。hosts設定はhosts_response_appと同様、引き続き別に存在しているが、自分はssl_optionsと同じような感じでHost Authorizationをグループ化してみた。グローバルなhosts_response_appは、Host Authorization失敗のレスポンスの一部でしか使われないのであれば非推奨化するのがよいかもしれない。既存のテストも更新してメソッドのシグネチャを変更し、この除外機能を検証するテストも追加した。
同PRより大意


つっつきボイス:「Rails 6に入ったHost Authorizationは、DNS Rebindingを悪用した攻撃への対策機能で、ドメインから来るリクエストのHostヘッダーもこの機能でチェックしてたと思います」

参考: Rails 6 adds guard against DNS rebinding attacks | Saeloun Blog
参考: DNS Rebinding ~今日の用語特別版~ | 徳丸浩の日記

「このプルリクでは、そのHost Authorizationで除外リストを指定できるようになった: サンプルコード↓にもあるように、healthcheckリクエストだけ通して欲しいことがよくあります」「Hostヘッダーがついていないリクエストでもこうやって除外リストを指定して通るようにしたいですよね↓」「これはやりたいです」

# actionpack/lib/action_dispatch/middleware/host_authorization.rb#12
 config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }

参考: Application Load Balancer とは - Elastic Load Balancing

「通常はX-Forwarded-ForヘッダーとX-Forwarded-Protoヘッダーがリクエストに追加されるので大丈夫なんですが、ALBのhealthcheckリクエストのようにVPCの中(つまりプライベートIP)から来るリクエストや、VPC Lambdaなどから呼び出すインターフェイスは、HTTPで直接アクセスします: このような設計は割とよくあるので、今回のように除外リストを指定することでそうしたリクエストもRailsで受信できるようになるのはいいですね👍」「ありがたい🙏」「これができないと不便なんですよ: 今まではhealthcheckを受信するためにALB healthcheck用のIPアドレス範囲設定が必要でした」

参考: X-Forwarded-For - HTTP | MDN
参考: X-Forwarded-Proto - HTTP | MDN

⚓ 新機能: RailtieのserverブロックでRailsサーバー起動後にコードを実行できるようになった

serverは、consoletaskといった他のRailtieブロックと似ている。目的は、サーバー起動後にアプリケーション(つまりrailtie)でコードを読み込めるようにすること。

ユースケースとしては、WebpackやReactサーバーをdevelopment環境で起動したり、SidekiqやResqueなどの一部のジョブワーカーを起動するなどが考えられる。

現時点ではこれらのタスクはすべて別シェルで実行される必要があるので、Railsサーバーの次に別のプログラムを起動する必要がある場合は、gemメンテナーがgemのライブラリの実行方法をドキュメントに追加する必要がある。
この機能はたとえば以下のように書ける。

  class SuperRailtie < Rails::Railtie
    server do
      WebpackServer.run
    end
  end

同commitより大意


つっつきボイス:「なるほど、サーバー起動後にRailtieでコードを実行できるようにするのか」「ジョブワーカーをここから起動したりすると書いてますね」

なおRailtieは、Railsのコアライブラリのひとつです↓(レールタイ: 線路の枕木(railroad tie)のもじり)。

参考: Rails::Railtie

「このように複数プロセスの起動や管理をつかさどるソフトウェアはいろいろあって、よく『プロセスマネージャー』と呼ばれたりします」「docker-composeとか」「Rubyのinvokerとか」「foremanもありますね」「powもあった」「自分は最近docker-composeでまとめて管理することが多いので、この手のプロセスマネージャーは使わなくなったな〜」「invokerはちょうど今使ってます、重いけど😆

docker/compose - GitHub
code-mancers/invoker - GitHub
ddollar/foreman - GitHub
postageapp/powr - GitHub

「今回追加されたserverは、そういうプロセスマネージャーっぽいこともRailtieでできるようになったということですね: ただこれを見る限りでは、サーバー起動後にフックをかけるだけのようで、これだけでは落ちたプロセスを再起動するなどのプロセス管理まではできなさそうですが」「起動はするけど管理まではしないのか…」「serverブロックにコードを書けるので、もっと複雑なこともやろうと思えば一応できますね」

「この機能は、サンプルコードのWebpackServer.runみたいに、サーバー起動後にこれとこれだけ実行しておきたいというようなシンプルな用途に便利そう: そのためにforemanなどのプロセスマネージャーをわざわざインストールしなくても済むので」「たしかに」

⚓Rails

⚓ Railsモデルのパターンとアンチパターン


  • (Fatと呼ばず)重量オーバーモデル
  • SQLパスタパルメザンチーズ風味
  • Repositoryパターン
  • マイグレーションの心得
    • downメソッドを必ず用意する
    • マイグレーションでActive Recordの呼び出しを避ける
    • データのマイグレーションとスキーマのマイグレーションは分ける

つっつきボイス:「AppSignalの記事です」「なぜかfat modelのfatに取り消し線を付けてoverweightとしてますね」「fatという言葉が使い古されてきたとかそういう意図なのかな?🤔

「SQL Pasta Parmesanも、もしかするとSQLがスパゲッティ状になることをわざとそう呼んでいるのかしら?」「パルメザンチーズ風味🧀

参考: スパゲティプログラム - Wikipedia

# 同記事より
class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.where(status: :published)
                .where(artist_id: artist_id)
                .order(:title)

    ...
  end
end

class SongController < ApplicationController
  def index
    @songs = Song.where(status: :published)
                 .order(:release_date)

    ...
  end
end

class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.where(status: :published)

    ...
  end
end

「↑上のようにwhere(status: :published)のような同じ条件をあちこちにバラまくよりは、下のようにスコープにまとめる方がいいですよね↓」「これはそうですね」

# 同記事より
class Song < ApplicationRecord
  ...

  scope :published, ->            { where(published: true) }
  scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
  scope :sorted_by_title,         { order(:title) }
  scope :sorted_by_release_date,  { order(:release_date) }

  ...
end

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.published.by_artist(artist_id).sorted_by_title

    ...
  end
end

class SongController < ApplicationController
  def index
    @songs = Song.published.sorted_by_release_date

    ...
  end
end

class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.published

    ...
  end
end

「この記事では、どうしても必要な場合を除いてRepositoryパターンはおすすめしないと書かれてますね」「どうしても使いたいならたとえば以下のようにSongRepositoryにまとめる方がマシだけど、それもおすすめはできないとも書かれてる」

参考: ドメイン駆動設計 - Wikipedia

# 同記事より
class SongRepository
  class << self
    def find(id)
      Song.find(id)
    rescue ActiveRecord::RecordNotFound => e
      raise RecordNotFoundError, e
    end

    def destroy(id)
      find(id).destroy
    end

    def recently_published_by_artist(artist_id)
      Song.where(published: true)
          .where(artist_id: artist_id)
          .order(:release_date)
    end
  end
end

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = SongRepository.recently_published_by_artist(artist_id)

    ...
  end
end

class SongController < ApplicationController
  def destroy
    ...

    SongRepository.destroy(params[:id])

    ...
  end
end

「言い換えると、Active Recordを普通に呼び出せば済むことを、わざわざRepositoryパターンでラップするなということなんでしょうね: DDD(ドメイン駆動開発)の文脈ではデータベースロジックをビジネスロジックから分離するためにRepositoryパターンのクラスを間にはさむことがありますけど、たしかに上でやっていることはActive Recordを呼び出せばできるので、ここではRepositoryパターンにしなくてもよさそう」

「なるほど、Active Recordと機能がかぶるSongRepositoryクラスをわざわざ作るのは、Active Recordをむしろ損なっているのではないかという考えかもしれませんね」「Active Recordでできない機能をRepositoryパターンで作る分には構わないと自分は思います」

なお、RepositoryパターンはHanamiで全面的に採用されています↓。

Hanamiフレームワークに寄せる私の想い(翻訳)

「ところでDDD本やデザパタ本を読むと、つい本のとおりにやってみたくなることって一度や二度はあるじゃないですか」「ああ、それ思い当たります😅」「Strategyが1個しかないのにStrategyパターンにしてしまったりとか」「Factoryが一種類しかないのにFactoryを作っちゃったりとか」「そういうノリでRepositoryを無意味に作らないようにしようということなのかもしれませんね」

「記事の後半はマイグレーションの心得についての話です」「マイグレーションにdownメソッドを書きましょうという話はそのとおりですね」

「その次は、マイグレーションの中で既存のActive Record継承モデルを直接参照するのは避けよう、という話」「ああ、これはやってはいけないとよく言われる」「Railsを長くやっている人ならやってはいけないことは知っていると思いますけど、Railsを最近始めた人だとやってしまうかもしれませんね」

「その次の、データのマイグレーションとスキーマのマイグレーションは分けようという話もそのとおり」「時間のかかるデータマイグレーションは切り離して、スキーママイグレーションでデータベースがロックされる時間が長くならないようにするべきですね」

「それほど目新しい内容ではないと思いますが、Railsで定番のアンチパターンや注意事項を押さえられるのがいいと思います👍」「Rails経験の浅い人によさそうな記事ですね」

⚓ listen gemが3.3.0でRuby 3やTruffleRubyに対応(Ruby Weeklyより)


つっつきボイス:「guard/listenはファイルの変更を検出するツール」「そういえば素のRailsにもlisten gemが入りますね↓」

# Rails 6で生成したGemfileより
group :development do
  gem 'web-console',           '4.0.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.1.0'
  gem 'spring-watcher-listen', '2.0.1'
end

「変更通知のインターフェイスはLinuxやWindowsやmacOSでそれぞれ違っているので↓、この種のファイル変更検出では複数のOSの抽象化をサポートすることが大事」「個別のOSのファイル変更通知を直接扱ったら面倒になりますよね」

⚓ Q:「WindowsでRailsを開発しますか?」

ここ3年ほど、Railsアプリ開発はmacOSでしかやっていないのですが、(リモートサービスの利用を考慮しなければ)あらゆる開発者はmacOSかLinuxのどちらかで開発しているように感じます。
そこで気になるのが、Windows上でのRailsアプリ開発がどれだけやりやすいかが気になっています。言語やシェル機能やデータベース管理などはWindows用ツールで十分提供されているのでしょうか?それともWSL 2やDockerでやる方がいいのでしょうか?
同discussionの質問より大意


つっつきボイス:「WSL 2を知らなかったとレスを付けていた開発者がいたのが気になって拾いました」「Sam SaffronさんはWSL 2はいいぞ、素のWindowsはつらいよとも書いてますね(レス)」「WSL 2はWindows上の選択肢としていいと思います、少なくともWindows版RubyバイナリでRailsを動かして開発するよりはずっといい」「同意です」

参考: WSL 2 と WSL 1 の比較 | Microsoft Docs

⚓ 個人のローカル環境構築は成果物にならない

「ちなみに自分がDockerをすすめる理由のひとつは、自分のローカル開発環境の構築ってどんなに一生懸命やっても成果物にならないからなんですよ」「そう、それですよね」「環境構築は楽しいんですけど趣味に近くなりがちなのと、環境構築に費やす時間はアウトプットが出ない時間でもあるので、DockerやVMなどを用いてより短い時間で環境構築を終える方が生産的だと思います」「そうですね」「もちろん、Windowsでの各種環境設定作業を自分でやることを厭わない人は、自分の好きな方法でやってもよいと思います」

⚓Ruby

⚓ スーパークラスのprivateメソッドをオーバーライドする


つっつきボイス:「親クラスにあるprivateなroleメソッドは、子クラスではprivateとして見えるけどpublicとしては見えない↓、これは普通」

# 同記事より
class Parent
  private

  def role
    'parent'
  end
end

class Child < Parent
  def get_role
    role
  end
end

Child.new.get_role # => "parent"

Child.new.private_methods.include?(:role) # => true
Child.new.public_methods.include?(:role)  # => false

「でも子クラスに同名のメソッドがpublicで存在していると、元クラスのprivateなroleメソッドは見えなくなって、子クラスのpublicなroleメソッドが見えるようになる」「へぇ〜」「結果として、親クラスのprivateなroleメソッドが子クラスのpublicなroleメソッドで上書きされて、get_roleの動作が変わるのか」

class Child < Parent
  def role
    'child'
  end
end

Child.new.get_role # => "child"

Child.new.private_methods.include?(:role) # => false
Child.new.public_methods.include?(:role)  # => true

「こういう書き方を実際に使うことってあるだろうか?」「親クラスのprivateメソッドを子クラスのpublicメソッドで上書きするのってちょっとトリッキーですよね😅」「こういうことができるとは…」

⚓ オブジェクト指向をどの言語で学ぶか

「ところで、Rubyのprivateメソッドは、JavaやC++などの他の言語のprivateメソッドと考え方が違っているところがありますよね」「自分はJavaから始めたのでJavaのprivateメソッドの印象が強いかも」

参考: JavaやC#の常識が通用しないRubyのprivateメソッド - give IT a try

「最近だと、オブジェクト指向言語(特にクラスを継承するタイプの言語)の仕様をどんな言語で学んでいるんでしょうね?」「あ〜、どうだろう…?」「この辺の概念って、最初にどの言語でオブジェクト指向を学ぶかで納得の仕方が違ってきそうな気がするんですよ」

「たとえばC++だと明示的にオーバーライドしないと別々の関数になるけど、Javaだと同じ名前で書けばデフォルトでオーバーライドされるといった違いがありますね」「C++はやったことなかったけどそういう仕様なんですか」「そういった書き味の部分は、外見上は似ていても内部実装が違っていたりします」

「あとRubyのprotectedがJavaやC++と考え方が違っているのも有名ですね」「protectedってどこで使ったらいいのかよくわからないかも」「使ったことありませんでした」「protectedを理解して使いこなすのは難しい…」「Gemやライブラリ開発をしないのであれば、privateとpublicで十分かもしれないという気がしています」

参考: Rubyのクラスメソッドは同じクラスのprotectedメソッドやprivateメソッドにアクセスできない - give IT a try

⚓ quine-relay: 128言語の生成チェインでウロボロスの蛇を形成(Ruby Weeklyより)

mame/quine-relay - GitHub


つっつきボイス:「@mameさん作の何だか壮大なクワインプログラムです」「クワインというと真っ先に@mameさんを連想するぐらい、@mameさんが強い分野ですね」


同リポジトリより

「こ、これは何ですか?😆」「Rubyプログラムを生成するRustプログラムを生成するScalaプログラムを生成する…を128言語繰り返して、最終的にRubyに戻るそうです」「トランスパイルに次ぐトランスパイルで元に戻る、まさにウロボロスの蛇🐍」「自分の尻尾は美味い😋

参考: ウロボロス - Wikipedia

「OCamlにBASIC、いろいろある」「zshやtcshまで!」「FORTRANやFORTHもある!」「知らない言語がゴロゴロ…」「言語が多すぎてulimit -s unlimitedしないと動かないっぽい」「最終的に生成されたRubyと元のRubyのdiffを取ると完全一致するんですって」「そこまでやりますか😆」「この完全に元に戻るという動作がクワインの一種ということなんでしょうね」

参考: 【 ulimit 】コマンド――ユーザーが使用できるリソースを制限する:Linux基本コマンドTips(326) - @IT

「この分だとquine-relayのソースコード自体もクワインだったりするかも?」「ソース見てみたら、ウロボロスの蛇があしらわれている…↓」「何だかクラクラしてきた😅

「クワインというものがあるって初めて知りました」「自分もRubyKaigiのアトラクション的なセッションでしか見たことがありませんでした」

参考: クワイン (プログラミング) - Wikipedia

「クワインって随分昔からあるみたいですね」「しかも聞くところによると、Webサーバーか何かで実用的なクワインの例というものがあるらしいですよ」「マジで?!」「あったあった↓、この記事ではクワインを遊びではなく機能として使っているそうです」「へぇ〜!」「これ以外で実用的なクワインの例は見たことがないですね」

参考: 自作Cコンパイラで Ken Thompson のログインハックを再現してみた - 0x19f (Shinya Kato) の日報

Thompson hack ではコンパイラ自身のプログラムが現れたら自分自身と全く同じコードを埋め込むという条件付きのクワインを使うことでソースコードから痕跡を消しつつ、セルフホストしても login を書き換える性質を引き継がせる仕組みになっています。
0x19f.hatenablog.comより

「ググって見つけた『あなたの知らない超絶技巧プログラミングの世界』という本がクワインだらけでした↓」「あ、これも@mameさんの本だ!」「クワインのスライド↓も@mameさんだ…」「なるほど、そういう方なんですね」

「なお@mameさんはIOCCC(国際難読化Cコードコンテスト)で何度も勝っています↓」「おぉ〜!」「IOCCCって今も開催されてるのか」

参考: The International Obfuscated C Code Contest
参考: IOCCC - Wikipedia

「@mameさんのアイデアの豊富さがスゴい」「以前RubyKaigiのコーナーで、どうやってこういうのを作ろうと思いつくんですかと質問されたときに、@mameさんがテクニックよりも面白いアイデアを思いつくかどうかが大事というような回答をされてた覚えがあります」「やはりネタが重要なんですね」

「このissueでのやりとり↓も好きです❤」「質問がWhy? Why?」「そして返信がWhy not? Why not?😆


つっつきの後で、@mameさんがRubyConf 2020のランチで書いたクワインが流れてきました↓。

quine-relayのビルドチェーンを見てて、何となく「これはジャックの建てた家」を思い出しました↓。こちらは循環していませんが。

参考: ジャックのたてた家 The house that Jack built :マザーグースの歌

⚓ その他Ruby

つっつきボイス:「RubyConf 2020、今日(=つっつきの日)が最終日なのか」「タイムゾーンが日本と真逆だから平日の日本だと見るの大変かも」

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓ POSTを冪等にするIdempotency-Keyヘッダの提案


つっつきボイス:「社内Slackに貼っていただいた記事です」「Idempotency-Keyは、はてブのコメントで『条件付きPUTを使えばできる』とありましたね↓」

POSTリクエストを冪等処理可能にするIdempotency-Keyヘッダの提案仕様 – ASnoKaze blog

冪等が目的なら現状でも条件付きPUTで実装できることが多いので、まずそれを検討したほうが良い。リクエストIDなら標準化しても良いと思う

2020/11/19 09:10

「元記事にもあるように、StripeやPayPalやGoogle Standard PaymentsなどでもIdempotency-Keyと似たようなことは既に行われているんですけど、Idempotency-Keyが標準として定まればそれを使う方がいいでしょうね(もちろんすべてのアプリケーションで使う必要はありませんが)」「今はまだ標準では存在しないんですか?」「request-id的なものを慣例的に冪等性の保証に使うことはありますが、処理の冪等性を保証することを目的とする専用のヘッダーはまだ存在しませんね」「なるほど」

「ドラフトRFCを見ると、Idempotency-KeyにはUUIDを使うことが推奨されている↓」「ただこのキーはクライアント(ブラウザ)側で生成するものなので、そこに関するセキュリティがどうなのか知りたい」「他にも、Idempotency Fingerprintが付けられることでデータだけを変更したreplay attackなどを防ぐようですね」

参考: draft-idempotency-header-00 - The Idempotency HTTP Header Field
参考: UUID - Wikipedia

「リクエストを再生するときに元のリクエストがまだ処理中の場合はHTTP 409 Conflict↓を返すとある: こういう振る舞いが標準で定められていると、APIドキュメントを読んだりAPI仕様を設計するときの手間が省けて助かりますね」「それわかります!」「409 Conflictは前からPUT用にあったけど、提案ではこれをPOSTでも使えるようにするようですね」

参考: 409 Conflict - HTTP | MDN

「このドラフトRFCは比較的短いので実装もそんなに大変ではなさそう: それも含めて自分はこのIdempotency-Keyの提案はなかなかいいと思っています👍」「Idempotency-Keyは必要がなければ使わなくてもよいのもいいですね」

⚓その他

⚓ Steamのインディーズゲーム

つっつきボイス:「pastalogicはプログラミングをお題にしたゲームだそうです」「これはカードを使う対戦型のボードゲームなんですね」「あ、カードに気が付かなかった😅」「そういえばSteam↓でときどきこんな感じのインディーズゲームを見かけます」「プログラミングをお題にしたゲームというものは見たことなかったかも」「Steamだとそれ系のゲームもちょくちょくありますよ」

参考: Steam - Wikipedia
参考: おすすめインディーゲーム27選。担当ライターが推す2019-2020冬の良作【後編】 - ファミ通.com

「インディーズゲームはアイディア勝負な分、ときどき驚くほどいいゲームが登場しますね」「そういえばFactorio↓をやっているとプログラミングしているような気持ちになって、あっという間に15時間ぐらい溶かしちゃいます」「Factorioはマイクラ(Minecraft)でレッドストーン回路を組んでいるようなものですよね」「はい、Factorioはそれに特化したようなゲームだと思います」(以下『天穂のサクナヒメ』の話など延々)

参考: Factorio - Wikipedia
参考: Minecraft - Wikipedia
参考: テクニック/レッドストーン回路 - Minecraft Japan Wiki - アットウィキ

⚓ Baba Is You

「ちなみにSteamのインディーズゲームの中でも、このBaba Is Youというパズルゲームは超名作です↓👍」「不思議な名前!」「何だか倉庫番がパワーアップしたみたいな画面ですね」「このゲームはたしか賞も取っていたと思います」

参考: Steam:Baba Is You
参考: 倉庫番 - Wikipedia

なおBaba Is Youはチューリング完全で、ライフゲームも実装できるそうです↓。


store.steampowered.comより

「基本はBabaというキャラクターを動かして、『Baba』『is』『you』や『Baba』『is』『win』などと並べる↑とクリアになるんですけど、たとえば『Rock』『is』『you』と並べると自分がBabaから岩に変わって岩を動かせるとか、『Lava』『is』『melt』にすると溶岩が溶け流れて通れるようになるなど、面ごとに意表を突くようなクリア条件が設定されているのがめちゃくちゃ面白い」「あ〜、勝利条件を書き換えることもゲームに含まれる感じなんですね」「はい、このゲームを最初にやるときは一切ヒントを見ないのがおすすめです」

「Baba Is You、任天堂SWITCHでも買えるのか、買っちゃおうかな(後で買いました)」「このゲームの面白さはぜひ体験してみることをおすすめします😋」(以下、ゲーム自作話など延々)


今回は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201117後編)Rubyのパターンマッチングが3.0で本採用に、AWS Lambdaサイズを縮小する、AppleのM1チップほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

iOS 13における必須対応について(更新版)

$
0
0

更新情報

  • 2019/08/28: 初版公開
  • 2020/11/25: 各項目について現在の状況を追記

⚓ はじめに

こんにちは、主にiOSアプリの開発を担当している川島と申します。

iOS 13のリリースが間近に迫りつつあり、またWWDC2019ではSwiftUIを始めとした新しいツール等の発表、ARKit2やCombineフレームワークなどの発表などにより、昨今のiOS界隈が盛り上がりを見せています。

そうした新しいツールや技術が登場する反面、Appleはバッサリとした互換切りや新技術への対応を短期間で強いる傾向にあり、既存プロジェクトの保守などをしているiOSアプリエンジニアはこの時期に頭を悩ませる人が多いのではないでしょうか。
2年前のSafeArea対応なんかは記憶に新しいですね。

今年もそうした「〇〇対応が必須」のような情報はチラホラと聞きますが、断片的な情報が多い印象です。

この記事では今後iOS 13(及びiPadOS)にて対応が必須となる、もしくはなりそうな件について、なるべく公式の情報を交えつつまとめて紹介します。

⚓ 注意

あくまでも執筆時点(2019/08/20)の情報を元にした記事となります。
現在はiOS 13のbeta版が出ている状態であり、今後公式ドキュメントなどを含めて変更・更新される可能性は大いにありえます。
そのため、必要な情報については適宜ご自身でご確認ください。

⚓ 1.「Sign In with Apple」の対応の必須化について

追記(2020/11/25)

他社のログインサービスやソーシャルログインサービスを使用するAppでは 2020年6月末より必須になりました。

Apple Watch Appは、watchOS 6 SDKかそれ以降のバージョンでビルドする必要があります。 ユーザーアカウントの認証や設定を行うAppは、App Store Reviewガイドラインのガイドライン4.8での要求に該当する場合、「Appleでサインイン」をサポートする必要があります。
developer.apple.comより

「Sign In with Apple」はセキュリティ及びプライバシー面を強化したAppleのサインインシステムになります。
また、端末でログイン済み状態であれば認証が簡略化できます。

6月のApp Storeレビューガイドラインのアップデート情報に以下の記載があります。(2019/08/20時点)

Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.
Updates to the App Store Review Guidelines - News - Apple Developerより

ソーシャルログインなど、サードパーティ製のサインインシステムを利用しているアプリには、今年の後半以降にログインオプションの選択肢の一つとしてこのシステムを導入することが求められるようです。
こちらの対応必須化は、ほぼ確定かと思われます。

対応にあたっては、ガイドラインが指定されており、サインインのボタンを目立つように配置する必要があります。

Button Size and Position
Prominently display a Sign In with Apple button.
Make a Sign In with Apple button the same size as other sign-in buttons,
and avoid making people scroll to see the button.
Sign In with Apple - Sign In with Apple - Human Interface Guidelines - Apple Developerより

ログインシステムに関しては、アプリ開発と担当が分かれている場合も多いため、サードパーティ製のサインインシステムを用いているプロジェクトに関しては、適宜情報共有を行ったほうがよいでしょう。

⚓ 2. Launch Storyboard、全ての画面サイズ、画面分割機能対応の必須化について

追記(2020/11/25)

iPhoneやiPad向けAppはiOS 13 SDKかそれ以降のバージョンを使ってビルドし、Xcodeの StoryboardでAppのLaunch Screenを使用する必要があります。
iPhone AppはすべてのiPhoneスクリーンをサポートし、iPad AppはすべてのiPadスクリーンをサポートする必要があります。
developer.apple.comより

Launch Storyboard、全ての画面サイズの対応については、2020年6月30日以降対応が必須となっています
画面分割機能対応については、今の所、Apple からの告知は特に無いようです 。

WWDC2019にてAppleが、2020年4月までに全アプリがLaunch Storyboard、全ての画面サイズのサポート、iPadにおける画面分割機能の対応を行う必要がある旨を宣言しています。(2019/08/20時点)

以下の二点の対応を行う必要があります。

  • Launch Imagesを廃止し、Launch Storyboardに移行する
  • iPadに対応している場合、画面分割機能を有効にし、フルスクリーン以外の画面対応

Launch Storyboardに対応すれば、全画面サイズの対応は行なえます。
公式が大まかな日付まで明示しているため、来年の4月にはほぼ確実に対応必須となるでしょう。

⚓ 3. ダークモード対応の必須化について

追記(2020/11/25)

今の所、状況は変わっておらず、強く推奨のままとなっています。

iOS 13 SDKでは、デフォルトでダークモードに対応している状態とみなされます。
標準のUIでは、ユーザがダークモードを選択すると外観が勝手に変更されてしまいます。

大抵のアプリでは標準のUIとカスタマイズしているUIを併用しているものと思います。
ダークモード未対応のアプリをそのまま出した場合には、ユーザの端末の設定によっては、奇天烈な見た目になってしまうでしょう。

これについては、Info.plistUIUserInterfaceStyleキーにlightを指定することで、アプリ内では常にライトモード扱いとなり回避可能なようです。

しかし、公式のドキュメントに以下の記述があります。(2019/08/20時点)

Supporting Dark Mode is strongly encouraged. Use the UIUserInterfaceStyle key to opt out only temporarily while you work on improvements to your app’s Dark Mode support.
Choosing a Specific Interface Style for Your iOS App | Apple Developer Documentationより

UIUserInterfaceStyleキーによる回避は、あくまでダークモードへの対応作業が完了するまでの一時しのぎに限られる」という内容になっています。

今期中に必須として対応を求められるかについては不明ですが、かなり強めの言い回しとなっているため、いずれは対応が必要になるかと思われます。

大きいアプリになってくると、ダークモードの対応にはかなりの工数を割かれる可能性が高く、また対応自体はアプリのアクセシビリティの向上にもつながるため、今のうちに検討をした方が良いでしょう。

⚓ 4. UIWebView の廃止

追記(2020/11/25)

App Storeは、2020年4月からUIWebViewを使った新しいAppの受付を終了し、2020年12月からUIWebViewを使ったAppのアップデートの受付を終了します。
developer.apple.comより

新規アプリについては、4月からUIWebViewを使ったアプリはリジェクトされるようになりました

Appのアップデートの期限を2020年末以降に延長します。正式な期限が決まり次第お知らせします。
developer.apple.comより

既存のアプリについては、2020年12月でアップデートの承認を終了する予定でしたが、おそらく、コロナの影響で期限が延期となるようで、現在の期限は未定となっています

昨年、Deprecatedになって記憶に新しいUIWebViewですが、iOS 13 SDKで廃止になるのでは?との噂が弊社内で流れました。
というのも、公式のドキュメントでは、

SDK iOS 2.0 – 12.0

との記載になっており(2019/08/20時点)、iOS 13 SDKでサポートする雰囲気が無いからです。

とりあえず、Xcode 13 beta6(執筆時点での最新バージョン)で実行してみたところ普通にUIWebViewを使えました。
ドキュメントの更新漏れか?と考えていましたが、調べてみたところそうでもなさそうです。

以下に、AppleのWebKitのアーキテクチャを担当しているBrady Eidsonさんの、6月5日にTwitterのつぶやきを転載します。

まだ、UIWebViewはiOS 13で存在しているが、将来のリリースで消えますとのこと。

少し古めのアプリであれば、UIWebViewを利用しているアプリは多いのではないでしょうか。
流石に今からXcode 11のbeta版の更新でUIWebViewがいきなり消えるのは考えにくそうですが、次のマイナーバージョンアップでばっさり削除などはありえるかもしれません。

まだ未対応のアプリについては、WKWebViewなどへの移行を検討する時期かもしれません。

⚓ 5. その他

⚓ presentViewControllerのデフォルト表示が変更

presentViewControllerで画面遷移した場合のデフォルトの表示が変わり、下のViewが見えるような状態になります。

UIModalPresentationStyleにautoが追加され、デフォルトのモードがautoとなっています。
確認したところ、

  • iPhoneの場合: pageSheet、fromSheet、popoverなど(複数のモードが同一表示)
  • iPadの場合: pageSheet

と同じ表示になっているようです。

ドキュメント: UIModalPresentationStyle - UIKit | Apple Developer Documentation

全画面表示をする場合はfullScreenに変えましょう。

viewController.modalPresentationStyle = .fullScreen

⚓ iPadOSのユーザエージェント

アプリエンジニアにはあまり縁がなさそうな話ですが、
beta版の現状ではiPadOSではmacOS Safariと同一のユーザエージェントを返してしまうようです。

ユーサエージェントでiPadかどうか判定しているケースなどでは、気をつけた方が良いかもしれません。
少し調べてみたところ、解像度からiPadかどうか判断する方法があるようです。

(詳しくは参考文献の「Quiita: ユーザエージェントでの iPad 判定関数 iOS/iPadOS 対応版(2019/08/20時点)」をご確認ください)

⚓ おわりに

昨年はiOSのバージョンアップによる既存プロジェクトに影響がありそうな問題というのはあまり多くなく、だいぶ落ち着いている印象でした。

今年はリリース直後は平穏な感じがしますが、年明け頃から既存プロジェクトへの影響がそこそこありそうな制約が公式から告知されそうですね。

⚓ 参考文献

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

Railsでは、属性の型を変更することも、デフォルト値を設定することもできます。

以下のように、Messageクラスにdatetime型のsent_at属性があるとしましょう。

class Message
end

> Message.type_for_attribute(:sent_at)
=> <ActiveRecord::Type::DateTime:0x00007fa01c1fd650 @precision=6, @scale=nil, @limit=nil>
> Message.new.sent_at
=> nil

このsent_at属性の型を以下の方法でintegerに変更できます。

class Message
  attribute :sent_at, :integer
end

> Message.type_for_attribute(:sent_at)
=> <ActiveModel::Type::Integer:0x00007fa01bd86ba8 @precision=nil, @scale=nil, @limit=nil, @range=-2147483648...2147483648>

default:オプションを指定すればデフォルト値も設定できます。

class Message
  attribute :sent_at, default: -> { Time.now.utc }
end

> Time.now.utc
=> 2020-10-15 12:10:16 UTC
> Message.new.sent_at
=> 2020-10-15 12:10:16 UTC

しかしここで問題なのは、Railsは属性にデフォルト値を設定するときに属性の型も変更しますが、そのときに正しい型が失われてしまうことです(訳注: 型がType::Valueになってしまいます)。

> Message.type_for_attribute(:sent_at)
=> <ActiveModel::Type::Value:0x00007fd16d255418 @precision=nil, @scale=nil, @limit=nil>

⚓ Rails 6.1での変更

Rails 6.1でこの問題が修正されました(#39830)。

class Message
  attribute :sent_at, default: -> { Time.now.utc }
end

> Time.now.utc
=> 2020-10-15 12:10:16 UTC
> Message.new.sent_at
=> 2020-10-15 12:10:16 UTC
> Message.type_for_attribute(:sent_at)
=> <ActiveRecord::Type::DateTime:0x00007fa01c1fd650 @precision=6, @scale=nil, @limit=nil>

上のように、デフォルト値に基づいてActiveRecord::Type::DateTimeが設定され、型が失われなくなります。

関連記事

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)

Viewing all 2990 articles
Browse latest View live