『現場で役立つシステム設計の原則』感想

読んだ。

雑多に感想を書く。

いわゆる Rails Way において到達できるところと、実際にシステムが要求するところの水準に乖離があるような現場はそこそこあると思っていて、Rails から入ったけどそういうところに困っている人(= つまり僕だ)にいい本だと思った。
ふだんなんとなく気にしていることが明文化されているのがよかった。

とりあえず本記事の読者層をRailsさわったことありますという人として、第8章(アプリケーション間の連携)以降はあまり読む必要はなさそうだった。本書のよさはそこにはなく、蛇足だと感じたが、それは普段どういった情報に触れているかという話でもあるので読む人によるのだろう。

たとえばいわゆる Rails Way の何が厳しいのか。それを語るには Rails 歴がたりなさすぎるのであるがそれはおいておくとして、1つの指摘として「REST表現におけるリソースとDBのテーブルが1対1に対応する」という素朴な前提がよく育ったアプリケーション(あるいは対象ドメインによっては)当てはまらないということがあると思う。

bin/rails g scaffold Post でうごいたわーいというのはプロトタイピングとして優秀であるのですが、たとえば post を作るのは1つのクチでいいのか、という話。
たとえば有料課金ユーザーにのみに許されるアフィリンク追加みたいな機能があるかもしれない、あるいは記事を「書く」のではなくツイートの切り貼りで1つの記事を作りたいかもしれない。もしかしたら記事自体を売りたいというような要求が出てくるかも。こういった posts テーブルにまつわる種々の事柄1をどう捌いていけばいいのかというのが Rails Way を超えた先にあるような気がしている。

Rails Way 以外のナニカに触れる機会として本書は非常によかったように思う。いま対峙しているアプリケーションにおいてどう設計するのがよいか、Rails Way と違う点は何かを考えるための機会を得たという感触がある。


  1. posts 以外のテーブルに入れたほうがいいような例もあった気はしつつ

秒速でサイト作ったら反響がよかったので振り返り

github.com

きっかけは、隣に座ってた人がいきなりやり始めたんだけど、検索ロジックとかが ガバそう 改善の余地がありそうだったので口出ししてたところから。

やったことは主にこんな感じです。

  • (PRになってないけど) 検索ロジックに口出しした
    • とりあえず \s+ で分けて、それぞれに対してマッチするかでANDとればいいと思うよ、と話した
    • むかーし slack の検索を自前でやろうとしたときに似たようなことをした
    • そのときは単語の順番も考慮したいので new RegExp(str.split(/\s+/).join('.*')) みたいな感じだった
    • 当時も今もパフォーマンスは気にしない前提
  • bulma 導入
    • element-ui を使おうとしてたっぽいけど設定ミスって中国語になってた
    • だったらもう bulma 使おうぜと提案して勝手に導入
  • use strict した
  • IE11対応
    • arrow
    • オブジェクトのメソッド記法を直した
    • String.protoype.includes がないので String.prototype.indexOf で代用
    • IE だと死ぬほど検索遅いんだけどターゲット層はIEユーザーが多いとのこと。はい
  • placeholder 追加
  • {{}} が見えないように v-text を使用
  • favicon 追加
  • strong とかを bulma 化

最初のほうはやることたくさんあって楽しいですね。スタイルはまだまだ貧弱なのですがそこは苦手な分野なので PR をお待ちしております。

その rescue_from ほんとに必要ですか

だいたい必要ないと思ってますよ、という話。


Rails アプリケーションの開発において rescue_from を追加することが正しいことは少ないと思っています。
rescue_from とは実際にはアクションおよびコントローラをまたいだ begin - rescue ( try - catch )であると言えます。
begin - rescue は大きく分けて2種類の使い方があると思っています。1つは、局所的な例外処理を書くとき。もう1つが例外からの復帰を諦めた場合に最後に(最初に)囲んでおくやつです。
ここでは前者を「復帰用rescue」、後者を「覆い隠し用rescue」と呼びます1

class Foo
  def fetch
    AwesomeHTTP::Get("https://example.com")
  rescue AwesomeHTTP::Error => e # ライブラリのエラーを拾っておく
    # 「復帰用rescue」 ( 局所的な例外処理 )
    raise MyApp::APIError, e
  end
end

begin
  Foo.new.fetch
rescue MyApp::Error => e
  # 「覆い隠し用rescue」
  # 復帰できないときには MyApp::Error あるいはそのサブクラスを投げるという規約にしておく
  warn "エラーが発生しました"
  warn e
  exit 0
end

rescue_from はこの使い分けでいうと「覆い隠し用rescue」であると言えます。根拠は、どこで起きるかは関係なくすべて拾ってユーザー側には通知しないようにしている点です。

Rails にはある程度のエラー処理を自動的にやってくれる仕組みがあります。Rails guide の rescue_responses を参照してください。

Configuring Rails Applications — Ruby on Rails Guides

rescue_responses を使わずに rescue_from のブロックで何か動的に処理をしたくなっている時点で「復帰用rescue」に該当していると思います。
しかし、復帰時の方法は果たしてコントローラ単位で本当に共通なのでしょうか?どこで起きた例外かによって処理を変えたいのでは?

というわけで rescue_from を書いておいたほうがいいことってほっとんどないと思うんですけどどうなんですかね。 ただの500ページより情報量が多いエラーページを出すために ApplicationController に書いとく、とかはアリなのかなあ。


  1. よい名前、あるいはこれに該当する分け方が既にあったら教えてください :bow:

jewel polisher

最近自分たちのプロジェクトが依存する gem 関連で何かが起きて対応が必要だったことが続いたのでメモっておく。

rails-html-sanitizer 0.4.0

CVE-2018-3741 対応。これは依存が多いので追うのがちょっとめんどかった。情報としては以下の issue が非常によくまとまっていたので置いておく。

github.com

libxml2 <- nokogiri <- loofah <- rails-html-sanitizer という依存関係で、大本は libxml2 の脆弱性
属性に HTML コメントとして解釈できそうな文字列が含まれたときに適切にサニタイズしないことがあるように読めた。1

これはかなり前に報告済み2 だがlibxml2 では修正されてないっぽい? とりあえず loofah 側ではこれを対応できるようにpublic なメソッドをはやしてくれている。

https://github.com/flavorjones/loofah/commit/56e95a6696b1e17a242eb8ebbbab64d613c4f1fe

rails-html-sanitizer はそれを使うようにした、という感じ

https://github.com/rails/rails-html-sanitizer/commit/f3ba1a839a35f2ba7f941c15e239a1cb379d56ae

該当コミットで追加されたテストケースを対応前のバージョンで走らせたらこんなだった。

  def test_uri_escaping_of_href_attr_in_a_tag_in_white_list_sanitizer
    html = %{<a href='examp<!--" unsafeattr=foo()>-->le.com'>test</a>}

    text = white_list_sanitize(html)

    assert_equal %{<a href="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>}, text
  end

# -"<a href=\"examp<!--%22%20unsafeattr=foo()>-->le.com\">test</a>"
# +"<a href=\"examp<!--\" unsafeattr=foo()>-->le.com\">test</a>"

対応としては普通に update すればおk

mysql2 0.5.0 リリース

リリースされたのだが Rails5 で mysql のバージョンをうまく制限していないと以下のコードで死ぬ。

https://github.com/rails/rails/blob/a0c43ce39b6b2ff2cd8f7fee07f809f34cad9b5b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb#L4

gem "mysql2", ">= 0.3.18", "< 0.5"

普通にしてると mysql2@0.5.0 が入ってきて「バージョンを満たす gem がない」などのエラーになる

対応は簡単で mysql2 のバージョンを固定しておけばよい。Railsと同様に "< 0.5" をつけておけばよいだろう。

現在の stable の先っちょでは対応済みっぽい3github.com

0.5.0 対応して大丈夫か、という判断があるので 5-1-stable ではバージョン指定で対応するかと思ったけど普通にゆるめてくれましたね。ありがたや。


普段自分の仕事内容を思い出そうとしても、こういう活動は忘れがちなので残しておいた。
アプリケーションというのは、機能を追加すれば複雑性が増すし、何もしなくても部品のメンテが必要である。
Rails バージョンアップなどの目立つものもあるが、目立たないこういったところで追随する努力も必要であるし意味のある仕事なんだぞと自戒をこめて書いておく。


  1. 同僚は https://github.com/GNOME/libxml2/blob/960f0e275616cadc29671a218d7fb9b69eb35588/HTMLtree.c#L714-L718 から a タグの href, action, src, name 属性のみかな?と言っていたが検証していない

  2. 2016-08-11 18:29 とのこと。 https://bugzilla.gnome.org/show_bug.cgi?id=769760 をみると “Can I ask again that the libxml2 maintainers consider addressing this issue?” などと言われているのが見える

  3. ライフサイクルよくわからんけど 5-0-stable には backport されてなくて気になる(のでコメントしている)

TaPL読書録 #3

前回: hkdnet.hatenablog.com

今回は4章終わりまで。
私物ノートPCのエンターキーがぶっ壊れているため今回のメモは控えめ。

定理3.5.12.

整礎集合の1要素へとマッピングできる関数 f を用いて無限降下列にならないことを示す論法は他の箇所でも見た気がする。

演習3.5.13.

(1) 雑に一意性を損ねること、正規形に関して影響を及ぼさないことがわかればよい。
(2) 1ステップ評価の一意性は損ねるが、多ステップ評価の一意性を損ねないところがミソ。

これは解答の補題とその訳注が面白い。

補題A.1.

ダイヤモンド性について。停止尺度の観点から評価によりかならず停止尺度が下がるのにもかかわらず、s -> t, s -> u において t -> u となるようなことがあるのか疑問に感じて発言した。が、停止尺度として示されていた項のサイズは必ず1ずつ下がるわけではないので十分にありえる話だった。

f:id:hkdnet:20180317144536j:plain

訳注について具体例をもって示す。 E-IFFALSEとE-FUNNY2が同時に成り立つような項について考える。

t2 -> t2' において if false then t2 else t3

E-IFFALSE を用いて評価した場合は t3 E-IFFUNNY2 を用いて評価した場合は if false then t2' else t3 であるがこれは再度 E-IFFALSE を用いて評価することで t3 となる。

これはダイヤモンドではなく、「三角形」であると言える。

f:id:hkdnet:20180317144552j:plain

本では次に数値という概念で拡張する。

prod 0 -> 0 なる評価に意図を感じる。

演習3.5.16.

演習としては行き詰まり状態と wrong が多ステップ評価の元同値であること、すなわち 「行き詰まり状態であれば wrong と評価されること」と「wrong と評価される前の項は行き詰まり状態であること」が示せればよい。

演習3.5.17.

こちらも iff であることを示せばよい

演習3.5.18.

E-IF, E-IFTRUE, E-IFFALSE を取り除いて5つの評価規則を追加する。画像参照。
t3の評価を行う評価規則の左辺で t2 ではなく v2 であるところに注意。

f:id:hkdnet:20180317144421j:plain

補題A.8.

f:id:hkdnet:20180317144644j:plain

succ t ->* succ v のときに t ->* v とステップ数が変わってないので「他の項構築子についても同様である」が誤りでないかという指摘があった。のだけど succ の付け外しも評価に含むんじゃねーかなといまは思いつつある。

次回は5章全部。

どうかくE22とjit

問題: 辞書順に数を並べる 2018.3.3

実装: github.com

とりあえずナイーブに解いたやつ。鍋谷さんは「Rubyでは解けないくらいの計算量にしたつもりだった」と言っていたが案外解けている。

$ ruby -v
ruby 2.6.0preview1 (2018-02-24 trunk 62554) [x86_64-darwin17]

MacBook Retina, 12inch, 2017 1.4GHz Core i7 メモリ16GB

$ time ruby test.rb
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
ok 7
ok 8
ok 9
ok 10
ok 11
ok 12
ok 13
ok 14
ok 15
ok 16
ok 17
ok 18
ok 19
ok 20
ok 21
ok 22
ok 23
ok 24
ok 25
ok 26
ok 27
ok 28
ok 29
ok 30
ok 31
ok 32
ok 33
ok 34
ok 35
ok 36
ok 37
NG: 0
ruby test.rb  829.58s user 107.90s system 91% cpu 17:05.94 total

jit

$ time ruby --jit test.rb
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
ok 7
ok 8
ok 9
ok 10
ok 11
ok 12
ok 13
ok 14
ok 15
ok 16
ok 17
ok 18
ok 19
ok 20
ok 21
ok 22
ok 23
ok 24
ok 25
ok 26
ok 27
ok 28
ok 29
ok 30
ok 31
ok 32
ok 33
ok 34
ok 35
ok 36
ok 37
NG: 0
ruby --jit test.rb  752.21s user 114.54s system 95% cpu 15:06.41 total

約10%の高速化、な気がする (あまりベンチマークに詳しくない)

別解法を考えた。枝刈りバンバンしていくことでなんとかならないか、というやつ。
そもそも実装がよくわからんけどミスっているのと「xが大きいときにはやくならないですね」という指摘を受けてばたんきゅ〜
n進数なのに木構造を使わなかったのはちょっとダメな気がした。

Ruby の ! について

はいどうも

! メソッド

Ruby における ! ってご存知ですか?
Ruby演算子っぽいようなものもメソッドとして実装されているという話があったりします。

1 + 1 # Integer の + メソッドが呼ばれている

class Integer
  # 上書き
  def +(other)
    42
  end
end

1 + 1 # => 42

! も BasicObject のメソッドとして定義されています。

/**
 *  call-seq:
 *     !obj    -> true or false
 *
 *  Boolean negate.
 *--
 * \private
 *++
 */

MJIT_FUNC_EXPORTED VALUE
rb_obj_not(VALUE obj)
{
    return RTEST(obj) ? Qfalse : Qtrue;
}
// ...略
    rb_define_method(rb_cBasicObject, "!", rb_obj_not, 0);
class BasicObject
  def !(*)
    raise 'bang dayo-nn'
  end
end

!1
# Traceback (most recent call last):
#         1: from basicobject.rb:7:in `<main>'
# basicobject.rb:3:in `!': bang dayo-nn (RuntimeError)

ところで

クラス内でのメソッド呼び出しについては以下のようになりますよね。

class Foo
  def foo
    bar(1) # Foo#bar への呼び出しになる
  end

  def bar(arg)
    puts arg
  end
end

Foo.new.foo # => 1 が出力される

ということはこれで Foo#! が呼ばれる気がするじゃないですか

class Foo
  def foo
    !(true)
  end

  def !(arg)
    puts '! called'
    false
  end
end

Foo.new.foo # なんもでない
puts 'done'

でもそうはならないんですよね。

なんで?

parseされたときの結果を見ればわかりますが、 ! メソッドのレシーバーは nd_lit: 1 であることがわかります。

$ ruby --dump=parse -e '!(1)'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,4))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_OPCALL (line: 1, location: (1,0)-(1,4))*
#     +- nd_mid: :!
#     +- nd_recv:
#     |   @ NODE_LIT (line: 1, location: (1,2)-(1,3))*
#     |   +- nd_lit: 1
#     +- nd_args:
#         (null node)

!(1) は メソッド ! 呼び出しの引数が 1 ではなく、(1) (= 1)への ! メソッド呼び出し扱いになるんですね。

parserの気持ちがわからない。
以上、昔この issue を見てよくわかんないなーと思って調べたことの dump です。

github.com

なおこの issue を理解するには更に if + regex literal のときの挙動を理解しておく必要があります。

また if の条件式が正規表現リテラルである時には特別に $_ =~ リテラル であるかのように評価されます。

https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fcontrol.html#if

補足

Ruby の動作は ruby 2.6.0preview1 (2018-02-24 trunk 62554) [x86_64-darwin17]
CRubyのソースコードは ac44ae58c697971e14e72493da2cfdd9a4e3b458 時点のものを基準としています。