Ruby で 1 < 2 < 3 みたいな比較演算を書けるようにする試み
TL;DR
華麗に失敗した。
発端
こんなモチベーションなので実用は目的にしていない。
何番煎じかもわからないけど、考えてみたかったので考えてみた。
問題をシンプルにする
仕様を絞る。
- 対象クラスを絞って
Integer
だけにする - 境界値を含まない場合だけを考える(
<
,>
だけ) ()
による演算の優先順位変更とかも考えない
アイデア
「左から順番に比較して、その結果を boolean じゃなく以前の 比較結果を保持するいい感じのオブジェクト で返せば良いのでは?」
左から順番に比較していくだけなら、
- 前回の比較が
true
だったのかどうか - 前回の比較における右辺
が取れればいけるだろうという感じ。
初めに考えていた方針
Comparable
をいい感じに魔改造すればいけそう- いい感じのオブジェクト は
TrueClass
,FalseClass
あたりを継承すれば良さそう
とぼんやり考えていたところ、
Comparable
を変更してもInteger
の挙動が変わらないTrueClass
,FalseClass
が Singleton なので継承するとnew
できない
あたりの問題が起こったので、Comparable
じゃなく Integer
を直接触るようにしつつ、比較結果オブジェクトをそのまま条件式で使えるようにするのは諦めて to_b
を呼んで boolean に変換するアプローチに変更。
実装
比較結果を保持するオブジェクトComparisonResult::TrueClass
, ComparisonResult::FalseClass
を作って、Integer#<
, Integer#>
がそれを返すように変更。
コードとしてはこんな感じ。
require 'singleton' module ComparisonResult class TrueClass def initialize(left, right) @left = left @right = right end def to_b true end def <(other) @right < other end def >(other) @right > other end end class FalseClass include Singleton def to_b false end def <(_) self end def >(_) self end end end class Integer def <(other) if (self <=> other) == -1 ComparisonResult::TrueClass.new(self, other) else ComparisonResult::FalseClass.instance end end def >(other) if (self <=> other) == 1 ComparisonResult::TrueClass.new(self, other) else ComparisonResult::FalseClass.instance end end end # < raise '1 < 2' unless (1 < 2).to_b == true raise '2 < 1' unless (2 < 1).to_b == false raise '1 < 2 < 3' unless (1 < 2 < 3).to_b == true raise '1 < 3 < 2' unless (1 < 3 < 2).to_b == false raise '2 < 1 < 3' unless (2 < 1 < 3).to_b == false raise '2 < 3 < 1' unless (2 < 3 < 1).to_b == false raise '3 < 1 < 2' unless (3 < 1 < 2).to_b == false raise '3 < 2 < 1' unless (3 < 2 < 1).to_b == false # > raise '1 > 2' unless (1 > 2).to_b == false raise '2 > 1' unless (2 > 1).to_b == true raise '1 > 2 > 3' unless (1 > 2 > 3).to_b == false raise '1 > 3 > 2' unless (1 > 3 > 2).to_b == false raise '2 > 1 > 3' unless (2 > 1 > 3).to_b == false raise '2 > 3 > 1' unless (2 > 3 > 1).to_b == false raise '3 > 1 > 2' unless (3 > 1 > 2).to_b == false raise '3 > 2 > 1' unless (3 > 2 > 1).to_b == true
感想
一応動く何かはできたけど、思っていたことは全然できなかった。
対象クラス・境界値・優先順位あたりはゴリゴリやればいけるはずだけど、 to_b
が生えているのが「これはない……」という感じ。
oneshot task に規約と仕組みを導入する
Cookpad TechConf 2018 の突発 LT で話した内容をもう少しちゃんと書いてみます。
アドホックなタスクの必要性
日々、サービスを運用しているとアドホックなデータの変更や処理の再実行などが必要になります。
典型的には以下のようなケースです。
- データマイグレーション
- サポート対応
- 不具合対応
- その他もろもろ
ここではこのようなアドホックなタスクを oneshot task と呼ぶことにします。
oneshot task をスクリプトとして書く
oneshot task をスクリプトとして書くといくつかのメリットが得られます。
似たようなことをする方法はいくつかありますが、それらの方法と比べたときの主なメリットは次のようなものです。
vs. 管理画面
管理画面をポチポチすれば対応できるものによっても場合によってはスクリプトを書く価値があります。
大量のデータを処理する必要があるときにはスクリプトを書いたほうが簡単確実ですし、スクリプトであればトランザクションなども使えるのでアトミックな更新をすることができます。 また、事前条件・事後条件をチェックした上で必要に応じてトランザクションをロールバックするなどより安全に配慮した処理を行なうこともできます。
vs. 手オペ
ここで手オペと呼んでいるのは本番環境で bin/rails console
するとか DB に直接 SQL を投げるとかそういったものです。
サービスの最初期など場合によっては便利な手オペですが、ミスを起こしやすくリカバリしにくいので基本的にはスクリプトにしておくべきです。
スクリプトにすることによって、テストを書くことができるようになり、PR にしてレビューできるようになり、Git に残るので過去の変更内容を見返すことができるようになります。
考慮すべきこと
スクリプト化するメリットを書きましたが、スクリプトにすることによっていくつか考えることが生じます。
ファイルをどこに置くのか
Rails には oneshot task の置き場所に関する規約はないので、どこに置くかを決めておく必要があります。
いつまでも残ったら邪魔じゃないのか
一時的に使用するスクリプトをリポジトリに入れることによって他のコードへの依存が発生します。 将来、依存されているコードを変更したくなったときに依存している箇所を書き換えてよいのかどうか逐一判断する必要が生じます。
ファイル名・タスク名の命名とかファイル作成とかが面倒
単純にファイルを作るのが面倒という話です。
稀によくあるタスクだと名前が衝突したりするのでぶつからない名前を考えるのが地味に面倒だったりします。
規約と仕組みをつくる
日々、スクリプトを書く必要が生じる中でいちいち上のようなことを考えるのは面倒なので規約と仕組みで解決します。
規約
置く場所を決める
決めの問題なのでどこでも良いんですが、Rails で oneshot task を書くとなると rake task が一般的だと思うので lib/tasks/oneshot/
に入れることにします。
そうすることによって、置く場所をいちいち考える必要がなくなるとともに、このディレクトリにあるファイルは(実行後であれば)消しても良いとわかるようになります。
命名規則を決める
ファイル名・タスク名の命名にも規約をつくり、規約によってファイル名・タスク名にタイムスタンプをつけるようにして YYYYMMDD_foo_bar.rake
というファイル名と oneshot:foo_bar_YYYYMMDD
というタスク名にします。
そうすることによって、命名がぶつかることを防止するとともに、タイムスタンプを元に消して良いかどうか判断しやすくなったり rm lib/tasks/oneshot/2017*
というような一括削除が行えるようになります。
仕組みをつくる
規約をつくったので、その規約にのっとってファイルをつくっていけば良いんですが、規約通りにファイルをつくるというのは明らかに人間よりもコンピュータが得意な仕事です。
そこで規約通りにファイルを作成するための仕組みをつくります。
幸い、Rails には generator の仕組みがあって、普段よく使っている bin/rails g hogehoge
を自分でつくれるのでこの仕組に乗っかって bin/rails g oneshot FooBar
できるようにします。
generator については以下のページによくまとまっています。
使いまわしたいので gem にした
リンク先を読んでもらうとわかるのですが、generator の仕組みは非常にシンプルで generator をつくるのは全然難しくありません。
ただ、複数のプロジェクトで同じような generator を何度も書くのは面倒なので、使いまわせるように切り出して oneshot_task_generator という gem に切り出しました。
使い方
$ bin/rake generate oneshot FooBar
という感じで実行すると lib/tasks/oneshot/20180205_foo_bar.rake
というファイルができます。中身はこんな感じ。
# Skeleton for this file was generated by `bin/rails generate oneshot FooBar` # For copy and paste: `bin/rake oneshot:foo_bar_20180205` namespace :oneshot do desc '' task foo_bar_20180205: :environment do end end
基本的には規約通りのディレクトリに規約通りのテンプレを吐き出すだけのシンプルなものです。
カスタマイズ
gem にする前に運用していたバージョンでは oneshot でよく使うコード(e.g. トランザクション)をテンプレに埋め込んでいました。
そういった簡単なカスタマイズもできるようにしています。
# config/initializers/ohesnot_task_generator.rb OneshotGenerator.configure do |config| config.body = <<-BODY ActiveRecord::Base.transaction do # Write transactional code here end BODY end
# Skeleton for this file was generated by `bin/rails generate oneshot FooBar` # For copy and paste: `bin/rake oneshot:foo_bar_20180208` namespace :oneshot do desc '' task foo_bar_20180208: :environment do ActiveRecord::Base.transaction do # Write transactional code here end end end
おわりに
自分が欲しいものを規約と仕組みに落とし込んで、しばらく運用してみたところ便利だったので gem にしたというお話でした。
技術的にも思想的にも別に大したことはしてないんだけど、こういう地味に便利になるみたいなのが結構好きです。
使ってみて「ここをこうして欲しい!」とか「英語がクソ」みたいなのがあれば issue なり PR なりいただければと思います。
モデリングについて
カフェで設計を考えていたらいろいろ思うところがあったので、カフェでのツイートを中心にしつつ自分向けにまとめてみる。
モデリングとは
ここで言うモデリングは「人間の営みをコンピュータで実装するために分割して名前をつける行為」くらいの意味を意図している。 また、普段書いているのが Web アプリケーションなので、その前提を含んでいる。
知性が足りないので、分割して名前をつけるだけの営みで手こずっている。
— イケメン(予定) (@s_osa_) 2018年1月21日
人間の営みを理解するためには分析や対話を通じて概念を見出していく必要がある。
そして、実装するためにはだいたいロジックとデータが必要なんだけど、良い振る舞いをするモデルを得るためにはどちらかに偏ることなく総合的に考える必要があると思っている。 プログラムにはプログラムの、データベースにはデータベースの都合があるが、どちらかに寄せてしまうともう一方の表現力がなくなったり得意なことが活かせなくなったりしてしまう。
これ自体は前から意識していたことだが、自分で思っているよりも遥かに頻繁に意識の切り替えを行なっていたことに気付いた。
自分がモデルを考えるときにドメインと RDB それぞれの視点を切り替えてることに気付いた。無意識かつめちゃくちゃ高頻度でやってる。
— イケメン(予定) (@s_osa_) 2018年1月21日
良いモデリングをするためには概念を発見する力を磨くとともに、プログラムとデータベースの両方に対する知見を深めていく必要がある*1。
モデリングの難しさと個人的な感覚
上の定義では「分割して名前をつけるだけ」のモデリングだが、個人的にはかなり難しいことだと思っている。
半ば冗談でよく言っているのだけど、これは人間が高性能すぎるのが悪い。
人間はパターン認識とかファジー的な何かが得意すぎるので、異なるものを同一視した上で曖昧に扱って脳のリソースを節約する説。
— イケメン(予定) (@s_osa_) 2018年1月21日
だから、直感に基いてモデリングすると、実は分割が足りなかったみたいなことがよく発生する*2。
考えれば考えるほど、同じだと思っていたものが実は同じじゃないというケースにぶつかる。
— イケメン(予定) (@s_osa_) 2018年1月21日
もちろん、初めからより良いモデルに到れれば良いんだけど、神ならざる身である我々が完璧なモデルをつくれるわけはないのでモデルを修正する必要が生じること自体は問題じゃない。 むしろ、現在のモデルが現実をうまく扱えないときはより良いモデルに至るチャンス。
こういった「初めから100%は無理」「少しずつ良くしていく」というような心境があって「ドメインを掘る」「モデルを磨く」といった語法を使うことが多い。
モデルは「つくる」とか「考える」じゃなく「掘る」とか「磨く」という感覚。
— イケメン(予定) (@s_osa_) 2018年1月21日
さらにポエミーなツイートを引用しておくと、こんな感じ。
ダビデ像は「余分な石を削ったら」できたらしいし、仏像を彫る極意は「木の中に仏様を見出してそうじゃないところを彫るだけ」らしいので、モデリングは彫刻に似ているのかもしれない。
— イケメン(予定) (@s_osa_) 2018年1月21日
モデリングの重要性
こんなにも難しいモデリングだが、モデリングを頑張ると何がうれしいのか。 大きく3つあると思っている。
- ドメインに対する理解
- 堅牢さ
- 柔軟性
ドメインに対する理解
モデルを考えていく過程でドメインを掘っていく必要がある。 この営みはだいたい「◯◯とは?」のような問いを通して概念を明確にしていく作業になるので、結果としてドメインに対する明確な理解が得られるようになる。
堅牢さと柔軟性
残る2つ、堅牢さと柔軟性は相反するように見えて相関しているのではないかという仮説が浮かんだのでまとめて書く。
明確に認識された概念に基づいたモデルは「それが何であるか」が非常に明瞭になっている。
そのため、各モデルに対してしっかりとした制約を課すことができるし、仕様変更や認識のアップデートが発生した場合にもどの箇所にどういった変更を加えるかが明白になる。
一方、適切に分割されていない貧弱なモデルを元に実装すると、柔軟性を確保するためにデータをゆるく持つことで対処する必要が生じてデータのカタさが失われるし、分割されていないモデルに対してロジックを追加していくと神モデル的な何かが生まれたりする。
難しさを伝える難しさ
ここまでいろいろと書いてきたモデリングだが、その難しさを人に伝えるのも難しいと思っている。
貧弱なモデルを元に実装してもとりあえずのところは動くし、生産性の測定などをするにしても生産性の測定自体が難しいのに生産性に対するモデルの影響となるともっと難しいので、価値を示しにくい。
また、仕事で行なったモデリングは大半の場合において公開できないだろうし、モデルはドメインがあって初めて存在するので単体で OSS にするなどは難しそうなので、外部から観測することも難しい。
重要なことだと思うからこれからもやっていきたいけど、これ給与は上がるんだろうか。
— イケメン(予定) (@s_osa_) 2018年1月21日
このあたりはなんとかしていかないといけないと思っている。
おわりに
コーヒー代830円の代わりに得たいろいろな気付きや思いをつらつらと書いてみた。
難しく大変な領域だと思うが、昔から好きな領域なので今後も引き続きやっていきたいと思う。