SQLアンチパターン 第三章を読んで
アンチパターン
行の存在の重複をさけるため、主キーを定めることが多くあります。idというカラムを作って主キーの役割をもたせることも多いです。
今回のアンチパターンは全てのテーブルはidというカラムを持たせる、というものです。Railsなんかでもデフォルトではidつけますね。
idカラムの特徴は
- 数字が入ります(正確に言うと32bit or 64bitの整数)
- 同じ数字が入ることはありません
当たり前だと思っていたidもアンチパターンになることがあります。
デメリット
- 冗長なキーができてしまう
ポケモンでテーブルを作成すると
id | pokemon_name |
---|---|
1 | フシギダネ |
2 | ゼニガメ |
3 | ヒトカゲ |
この場合nameは必ず一意になりますしUNIQUE制約もつけられるのでidカラムは必要なキーとは言えなくなります。
また他のテーブルの主キーの組み合わせのを保存する交差テーブルを作っる時にその組み合わせにidをふっても組み合わせの一意にはならないので、組み合わせ2つのカラムにUNIQUEをつける必要がありますがそうなるとidカラムの意味は…ということになります。
- idという名前はかならずしもわかりやすくない idよりpokemon_nameのほうがわかりやすいです。
- USINGが使用できない カラムにpokemon_nameを持ってる例えばTrainersテーブルではUSING(pokemon_name)と使えますが全てのテーブルの主キーがidだとTrainersもidがあるのでusingにidが使えません。
使用してもいい場合
ORMフレームワーク(Ruby on Rails等)では設定より規約ということで、idという整数の疑似キーのカラムがある前提で便利な機能がついています。レールから外れるとその分大変です。ですが疑似キーをid以外の名前(pokemon_idとか)にすることもできることは覚えておいたほうが良さそうです。
Railsも主キーを変更することは公式でサポートされています。
https://railsguides.jp/active_record_migrations.html
解決策
状況に応じて適切に調整しようとあります。そりゃそうだという気持ちになったのは私だけでしょうか。
- 主キーをわかりやすい名前にする
idをproduct_idにするとかですね
- 自然キー、複合キーの活用
例のpokemon_nameのように自然に主キーとして使えるものもあるでしょう。こういった自然キーがない、もしくは将来的に信用できない時に擬似キーを使うべきです。 複合キーもカラムの組み合わせが行を識別する時に最適というときには使うといいでしょう。
感想
規約は必要不可欠というわけではない、柔軟に対応しようねといった章だと思いました。 idというカラムを主キーにすることが悪というよりはそれ以外の主キーの設定もあると知らないのが悪という感じですね。そもそも主キーが連番である必要もデータ型である必要もないというのをデータベース実践技術入門で読んだことがあります。
SQLアンチパターン 2章
アンチパターン
この章ではツリー指向のデータ構造を格納する際のアンチパターンになります。
ツリー指向のデータ構造では、ノードは一つ以上の子ノード及び一つの親ノードを持ち一番上がルート、一番下がリーフとなります。文章だと難しいので視覚で、
このような構造です。ディレクトリなんかと同じ構造ですね。 この構造を格納するためにparent_idカラムをつくり格納すると以下のようになります。
parts_id | parts_name | parent_id |
---|---|---|
1 | SQLアンチパターン | |
2 | 一章 | 1 |
3 | 二章 | 1 |
4 | 一節 | 2 |
5 | 二節 | 2 |
6 | 一節 | 3 |
7 | 二節 | 3 |
8 | 一項 | 6 |
この構造は隣接リストと呼ばれています。これが今回のアンチパターンです。
デメリット
- 全ての子ノードを取得したい時に(例えば東京から下のノードをすべて取りたい時)に一つ下の子ノードしか辿れないので、一つ一つジョインしていくしかありません。これでは階層が深くなった時に対応できません。
- ノードの追加や移動は簡単ですが、削除するには削除したいノードの子ノードも全て探して削除しなければなりません。またノード単体を移動したいときも、子ノードは一つ上の親に依存しているので親ノードが移動すると子ノードとその下のノードは全てツリーが移動してしまいます。
使用してもいい場合
ノードの削除が要らなかったり親と子だけ別れば良いときには使用しても問題ありません。家系図なんかは追加はしても削除はしないので、使用しても問題はないと思います。
解決策
他のツリーモデルを利用することです。本では3つの構造があげられています。
経路列挙
parts_id | parts_name | parent_path |
---|---|---|
1 | SQLアンチパターン | |
2 | 一章 | 1 |
3 | 二章 | 1 |
4 | 一節 | 1/2 |
5 | 二節 | 1/2 |
6 | 一節 | 1/3 |
7 | 二節 | 1/3 |
8 | 一項 | 1/3/6 |
UNIXのパスと同じ形で上のツリーのノードを記録する構造です。 この構造のメリットは子ノードを簡単に取得できます。1/3/%といった形で一章の子ノードを階層がいくら深くても全て取得できます。
入れ子集合
parts_id | parts_name | nsleft | nsright |
---|---|---|---|
1 | SQLアンチパターン | 1 | 16 |
2 | 一章 | 2 | 7 |
3 | 二章 | 8 | 15 |
4 | 一節 | 3 | 4 |
5 | 二節 | 5 | 6 |
6 | 一節 | 9 | 12 |
7 | 二節 | 13 | 14 |
8 | 一項 | 10 | 11 |
子孫の集合に関する情報をそれぞれの行に入れています。
nsleftには下のノード全ての値より小さい値、nsrightには逆に下のノード全ての値より大きい値が入っています。
ノードAの下のノードを全て特定したいときはAの子ノードの範囲内にnsleftが入っているノードを検索することで達成できます。逆にAの上のノードを全て検索したいときはAのnsleftを範囲内に持つノードを検索することで取得できます。
またあるノードを削除しても子ノードや親ノードを検索することに不都合はなく、削除されたノードの子ノードは親の親ノードの子であるとみなされるので削除も容易です。
しかしノードの挿入や移動は他の構造より複雑になります、全てのノードが親だけに依存しているわけではないので移動するといちいち値の計算のし直しです。
閉包テーブル
parant_id | child_id | parant_id | child_id | parant_id | child_id |
---|---|---|---|---|---|
1 | 1 | 2 | 2 | 5 | 5 |
1 | 2 | 2 | 4 | 6 | 6 |
1 | 3 | 2 | 5 | 6 | 8 |
1 | 4 | 3 | 3 | 7 | 7 |
1 | 5 | 3 | 6 | 8 | 8 |
1 | 6 | 3 | 7 | ||
1 | 7 | 3 | 8 | ||
1 | 8 | 4 | 4 |
テーブルの親と子の組み合わせを深さに関係なく全て格納するテーブルを作成する構造です。ノード自身を参照する組み合わせも格納します。前回の関係のためだけの中間テーブルに似ている気もします。
下のノード全てを検索するのは簡単でparant_idが4の組み合わせを取り出して元のテーブルをjoinすればいいです。先祖もchild_idから特定できます。
一ノードを追加するとその親とその上のノード全てを取得して新しいノードをchild_idとして登録することになります。削除では削除したいノードをchild_id持つ行を全て削除することになります。全て削除とかは機械は得意なのでいいですね。
もちろん閉包テーブルを削除しても章の情報は残ったままです。これはノードの関連付けを柔軟にできることにも繋がります。
どの設計を使うべきか
それぞれメリット・デメリットあります。
- アンチパターンとして挙げられた親ノードだけ持つ隣接リストは、下のノードがすぐわかればいい、削除等は基本行わないという用途に適していると思います。上記にも書きましたが家計なんかでしょうか。
- 親ノードまでの階層の道のりを全て持つ経路列挙は、道のりに変なものが入らない保証はできない、階層の深さに制限がかかる、といったデメリットがあります。住所なんかはパンくずリストから一気に参照したいことが多いでしょうし適していると思います。
- 子ノードの範囲を行に入れる入れ子集合は、削除はできますが挿入がしにくいです。ツリー構造の中のツリーを検索することが容易なので、本の分類なんか適していると思います。社会学とか数学とか分類は決まっていて、数学のツリーを見たいといった操作に適していると思うからです。
- 子ノード及びその下のノード全ての関係を別テーブルに保存しておく閉包テーブルは、検索や削除、挿入容易ですし、外部キー制約が使えるので整合性も良いですが、別のテーブルを使う、行数そのものは多くなるという欠点が生じます。これは用途というよりスペックの問題な気がします。
感想
この章はだいぶアルゴリズムを感じました。
調べる中でアルゴリズムの学習らしき論文っぽいものが多くでてきて、「これがアルゴリズムを学ぶことなのか」と感じました。実際SQLアンチパターンでも最後にアカデミックな資料をおすすめされています。
階層に対するクエリの実行だとかはデータベースとビューをつなぐ役割を持つバックエンドとしては必須の知識感ありますね(まだ全くないんですが…)
ここ理解違うんじゃない?みたいなのがあれば是非お願いします!
SQLアンチパターン 第一章を読んで
アンチパターンのテーブル例
Books
book_id | book_name | tag_id |
---|---|---|
1 | SQL-anti-pattern | 1, 2 |
2 | Readable Code | 2 |
Tags
tag_id | tag_name |
---|---|
1 | SQL |
2 | O'Reilly |
このテーブルの関係では、本はタグを複数持っていてタグは複数の本につけられています。この多対多の関係を、中間の関係を示すテーブルなしで外部キー、つまりtag_idを一つのフィールドにカンマ区切りで格納しています。
これが今回のアンチパターン「信号無視」です。
デメリット
特定のタグをもつ本を探したくてもtag_id = 1 のような形で探せないため、1があるかを正規表現なんかでパターンマッチする必要があります。これだと式の組み方を間違えてしまう可能性も高く、そもそも式の組み方がMySQLやPostgreSQL等RDBMSで違ってきます。
特定の本についているタグを結合したくても、その本のtag_idフィールドでパターンマッチしなくてはならないので効率的ではありません。
集約クエリ(COUNT等)は複数の行に対して行うように設計されているので、例えば本についているタグの数を数えたい時なんかにそのまま使用ができません。
tag_idをカンマ区切りのリストの末尾に追加するのは必ずしも番号順ではないためソート順が維持できなくなるかもしれません。一つタグを削除するのも古いtag_idをまるごと捨ててと新しいtag_idをまるっと入れねばなりません。
tag_idにはVARCHARで文字列なら何でも入れられます。危険です。
リストの長さがVARCHAR(30)等で簡単に限られてしまいます。
使ってもいい場合
なにかで区切られたデータが必要で、かつその個別の要素にアクセスが不要な場合はこの方法を用いても良い場合に挙げられています。
少しそんな場合を考えたのですが、多対多でそんなケースありえるんでしようか? 思いつきませんでした。
解決方
交差テーブルを作成する。いわく中間テーブルってやつですね。
BookTaggingsテーブルとかでしょうか。
book_id | tag_id |
---|---|
1 | 1 |
1 | 2 |
2 | 2 |
こうすることでデメリットを以下のように解決できます。
1及び2
特定の本のタグを検索するときにはTagsテーブルを結合してbook_idで絞り込めばいいですし、その逆もしかりです。インデックスを有効に使えます。3
行が別になっているので、例えば本についているタグの数を数えたい時はbook_idでGROUP BYしてそのまま数えることができます。4
BookTaggingsテーブルに行を挿入、もしくは行を削除すればいいだけです。5
フィールドの内容が同一なので、BookTaggingsテーブルのbook_idカラムにはBooksテーブルのbook_idしか入れられないという外部キー制約をかけることができます。6
一つのテーブルに格納できる行数以外の制約はもうありません!
感想
中間テーブルって何で必要なんだろう?という疑問をSQLの視点から答えてくれる章でした。中間テーブル、大事です。まだ業務では作ったことないですが。
情熱プログラマーを読んで
はじめに
僕が好きなVtuberできりみんちゃんさんという方がいます。小学生です。
話し方から伝わる人柄の良さとか努力の過程を公開している感じが好きです。この方が動画でおすすめしていた本が情熱プログラマーという本です。
エンジニアとしてのあるべき姿勢とそこにたどり着く具体的なアクションについて書かれています。
ただ自分は職業エンジニアになってからまだ一週間なので、実感できない部分もありました、なので全ての感想というよりは、少ないですが自分の中で納得感の大きかったトピックの感想を書いていきます。
一番の下手くそでいよう
このトピックでおすすめされているアクションに転職等がすぐに難しい人向けの具体的な方法に、優秀そうな人のOSSを読んで周りのコードを真似しながらなにかコードを書いてみようというものがありました。
自分が一番下手くそな状況というのは、自分以外に優れている誰かがいないと成立しない状況なので、実際にOSSで公開されている自分より経験のある人が書いたコードの中でコーディングするのは、転職等よりも遥かに敷居低く自分を一番下手くそにできると思います。
自分ひとりだけで自分が一番下手くそな状況というのはありえないので、独学でもこの姿勢は大事にしていきたいですね。本を読むのもこれが理由だと思います。語学とか他の勉強でも使えそう。
現職も間違いなく自分が一番下手くそなので、恵まれていると思っています。
新しい言語を学ぶ
自分の知性に投資しようというトピックにあった具体的なアクションです。
勉強するというより面白そうだからやるっていう言語のほうが良いみたいですね。
自分はKotlinに手を出しています。アプリをつくりたいという欲求と、今のスマホのOSがAndroidなことと、きりみんちゃんが推してるのが理由です。とりあえずKotlinスタートブックを買ってみました。
Rubyぐらいしかある程度時間を投資した言語がないので、そもそも静的型付け言語みてもよくわからないという悲しさが現在あります。その解消にもなるかも。
よくスマホみたまま寝落ちして睡眠時間がよくわからない感じになるので、スマホの画面つけている時間とつけてない時間記録して寝ている時間を計測してまとめてくれるアプリ欲しい‥
あわてるな
パニック日記!この発想はなかったです。
私は自覚があるくらいにはよくパニックになると思っているのですが、振り返ると、人に評価されている、値踏みされていると感じる時が一番パニックになっていると思います。悪い評価になるのが怖いし自分をできるだけよく見せたいからです。
このパニックをより細かく噛み砕けたら良さそう。カレンダーに早速スケジュールを追加してみました。
一年後はまた別の感想になりそう
開発者として使命を持つ、とかビジネスを知る、といったトピックはまだピンと来なかったです。
自分が開発者として少し経験を積んだらまた違った感想を持つと思うので、時間立ってから読み返したいと思った一冊でした。ベターエンジニアを日々目指していきたひ。
メタプログラミングRuby 6章コードを記述するコード
eval(string)
evalメソッドに文字列を渡すと実行してくれます。
irbもevalで実行されています。
式展開してメソッドを定義したいときなんかに便利。
しかし文字列をコードとして実行してしまうということはコードインジェクションに弱くなってしまうので、evalには自分で設定するもの以外は渡さないほうが無難です。
手順4のクイズ
require ‘test/unit’ module AttrCheckedModule def attr_checked(*attribute*, &*validation*) define_method(“#{attribute}=") do |value| raise 'Invalid attribute' unless yield(value) instance_variable_set("@“{attribute}",”value) end define_method("“{attribute}") do instance_variable_get("@#{“ttribute}") ” end end end class Person extend AttrCheckedModule attr_checked :age do |v| v >= 18 end end class TestAdd < Test::Unit::TestCase def setup @bob = Person.new end def test_accepts_valid_values @bob.age = 20 assert_equal 20, @bob.age end def test_accepts_invalid_values assert_raises RuntimeError, ‘Invalid attribute’ do’ @bob.age = 17 end end end
書のようにClassクラスのインスタンスメソッドとして定義してもいいですが、少し汚し過ぎかなと思うのでextendでクラスメソッドにしてみました。
フックメソッド
クラスの継承やモジュールのインクルードのイベントが起きた時に、それをキャッチしてメソッドを実行することができます。
class String def self.inherited(*subclass*) puts “#{self}は#{subclass}に継承された" end end class Mystring < String; end # -> StringはMystringに継承された
モジュールをミックスインした時やメソッドを作ったときなんかもフックできます。
クイズで
module CheckedAttribute def self.included(klass) klass.class_eval do extend CheckedAttributeMethod end end
と書きましたが、extendはクラスメソッドなのだからそのまま実行させればよかったですね。
7章読んでのポエム
メタプログラミングというものは存在しない
全てはただのプログラミングじゃ。
まだ自分は悟りを開けてはいない。しかしこの本を読んでいなかったら読めないコードは確かに存在したので、忘れられるほどは賢くないのだろう。
メタプログラミングRuby 5章 クラス定義
特異メソッド
カレントクラス
Rubyのプログラムは常にカレントオブジェクトselfを持っているが、同時にカレントクラスも持っています。
class C def m1 p self def m2; end end end class D < C; end obj = D.new obj.m1 # => <D:0x00007fc1e2865938> p C.instance_methods(false) # =>[:m1, :m2] p D.instance_methods(false) # => []
メタプログラミングRubyのサンプルに少し書き足してみました。 カレントオブジェクトはDクラスのインスタンスですが、カレントクラスはCクラスであることがわかりますね。よってm2はCクラスのインスタンスメソッドになっています。
Classでクラスを開くことでカレントクラスを変えることができるのですが、この方法はカレントクラスにしたいクラスの名前がわからないと使えません。クラス名がわからない時に使うのが class_evalメソッドです。
def add_method_to(a_class) a_class.class_eval do def m 'Hello!' end end end add_method_to(String) p "string".m # => 'Hello!'
このadd_method_toメソッドではクラスを引数にとってclass_evalを使うことで引数にとったクラスにカレントクラスを変更してからメソッドを定義しています。 class_evalは第四章のinstance_evalとは別物です。 instance_evalはselfを変更してブロック内を評価してくれるものでしたが、class_evalはselfに加えてカレントクラスも変更します。 スコープゲートを開いているわけではないのでスコープは変わりません。
クラスインスタンス変数
クラスのインスタンス変数とクラスのオブジェクトのインスタンス変数は別物です。
class Myclass @my_var = 1 def self.read @my_var end def write @my_var = 2 end def read @my_var end end obj = Myclass.new p obj.read # => nil obj.write p obj.read # => 2 p Myclass.read # => 1
ここでは2つの異なるオブジェクトである@my_varを定義しています。 クラスもオブジェクトの一つであるということを念頭において考えて行くのが大事になってきます。 最初にでてくる@my_varがクラスインスタンス変数。2つ目@my_varが今回だとobjオブジェクトのインスタンス変数ですね。 (ここメタプログラミングRubyでは1つ目の@my_varがobjのインスタンス変数で2つ目の@my_varがMyclassのクラスインスタンス変数だ、と書かれているのですが、コードの順番で行くと逆なので少しわかりにくかったです。説明の順番としては仕方ないのかもしれないですが。) クラスインスタンス変数はClassクラスに属しているオブジェクトのインスタンス変数というのが正しいです。MyclassはClassクラスのインスタンスなのでその中で定義されたものはクラスインスタンス変数となります。 クラスインスタンス変数はクラスしかアクセスできず、そのクラスのインスタンスからアクセスすることはできません。
特異メソッド
Stringに属したオブジェクトが全て大文字化かどうかをbooleanで返すメソッドをつくりたいが、特定の文字列にしかこのメソッドをもたせたくない、という場合。Refinementsを使って、特定のコードの前にusingを挿入するという方法もありますが、別の方法もあります。それが特異メソッドです。 同じクラスのオブジェクトでも、ある特定のオブジェクトにだけメソッドを追加したい、という場合に特異メソッドを使うことができます。
str = "string" def str.title? self.upcase == self end p str.title? # => false p str.methods.grep(/title?/) # => [:title?] p str.singleton_methods # => [:title?]
今までクラスに定義していたクラスメソッド
Class Hoge def self.a_method end end
これはクラスもオブジェクトであるという点からみれば
Hoge = Class.new def Hoge.a_method end
という形とやっていることは同じです。つまりクラスメソッドはクラスの特異メソッドであるという事になります。
クラスマクロ
attt_*のようなメソッドをクラスマクロと呼ぶ。selfがクラスでも使えるのでクラスメソッドということですね。
特異クラス
特異メソッドがどこに定義されているのか?という問の答えが特異クラスです。
obj = Object.new p obj # => #<Object:0x00007f9fa893a068> p obj.singleton_class # => #<Class:#<Object0x00007f9fa893a068>> singleton_class = class << obj p self # => #<Class:#<Object:0x00007f9fa893a068>> end
この例では<Class:#<Object:0x00007f9fa893a068>>がobjオブジェクトの特異クラスです。 class << objでも発見することができますが、singleton_classメソッドを使ったほうが楽でしょう。 オブジェクトのメソッド探索を行う際にはオブジェクトのクラスをみてそして更に親のクラスへとたどって行きますが、実は、オブジェクトのクラスのメソッドを見る前に、そのオブジェクトの特異クラスのメソッドを見にいっているのです。 クラスにもクラスはあるので、当然特異クラスがつくれます。
class C class << self def a_class_method 'C.a_class_method()' end end end
これは要は
class C def self.a_class_method 'C.a_class_method()' end end
と同じで、クラスメソッドはそもそもそのクラスの特異クラスに定義されています。
p C.singleton_class.superclass # => #<Class:Object> p C.superclass # => Object
このようにクラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスになります。
大統一理論
Rubyのオブジェクトのルールとして7つのルールが示されています。
- オブジェクトは一種類しかない。それが通常のオブジェクトかモジュールになる。
- モジュールは一種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
メソッドは一種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。 一種類しかないということは複数から同時に継承しないということです。下記の5にありますが、必ず親から子への一直線以外に継承はしていません。
全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。 メタプログラミングRubyでのいわゆる「右へ」の移動で、本物のクラス、つまり特異クラスをもっているということですね。ほとんどのオブジェクトは特異クラスをもっています。true, false, nilは持っていません。(NilClass等はもっていますが。)
すべてのクラスは(BasicObjectを除いて)一つの祖先(スーパークラスかモジュール)を持っている。つまりあらゆるクラスがBasicObjectに向かって1本の継承チェーンをもっている。 これと1,2,3のルールのおかげでメソッド探索はわかりやすいものになっていると思います。継承チェーンは二股に別れたりはしていません。
オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである
- メソッドを呼び出すときはRubyはレシーバーの本物のクラスに向かって右へ進み、継承チェーンを「上へ」進む。 この2つからなぜインスタンスがクラスメソッドにアクセス出来ないかを説明することができます。 クラスメソッドはそのクラスの特異クラスに定義されています。 Rubyのメソッド探索では一度だけしか右に行かないので、インスタンスからメソッド探索をおこなった場合、メソッドの特異クラスへと一度右に行った後はその特異クラスのスーパークラスであるobj.classが戻すクラスから上に探索していくので、そのスーパークラスの特異クラスまでは探索しません。よってスーパークラスの特異クラスにあるメソッドはインスタンスからはアクセスできないということになります。
Object#extend
モジュールをincudeするとモジュールのインスタンスメソッドのみがincludeしたクラスでインスタンスメソッドとして使えるようになりますが、 クラスの特異メソッドを開いて、その中でモジュールをincludeするとモジュールのインスタンスメソッドをクラスメソッドとして定義できます。 オブジェクトの特異クラスにも同様にでき、よく使われるので、Object#extendとしてメソッドが提供されています。
module MyModule def my_method; p 'hello'; end end class MyClass extend MyModule end MyClass.my_method # => 'hello'
メソッドラッパー
アラウンドエイリアス
class String alias_method :origin_reverse!, :reverse! def reverse! upcase! origin_reverse! end end p "ruby".reverse! # => "YBUR"
エイリアスを定義することで、メソッドの再定義を行う時に、元のメソッドをエイリアスメソッドで呼び出して、ラップすることができます。
ここでは古いreverse!
を新しいreverse!
にラップしています。一種のモンキーパッチにも近い方法です。
新しいreverse!
は古いreverse!
の周囲(アラウンド)をラップしているのでこのトリックをアラウンドエイリアスをと呼びます。
Refinements
Refinementsを使ってsuperを呼び出すと元のメソッドを呼び出せます。
module StringRefinements refine String do def reverse! upcase! super end end end using StringRefinements p "python".reverse! # => "NOHTYP"
Prependラッパー
module ExpilcitString def reverse! upcase! super end end String.class_eval do prepend ExpilcitString end p "go".reverse! # => "OG"
Prependは継承チェーンの下にモジュールが挿入されるのでメソッド探索の際にはprependされたモジュールにあるメソッドが先に使われる。
5章のクイズ
最初はsuperで解決しようとしたんですが、古い+に依存しているのでなかなか書けませんでした… メタプログラミングRubyのようにアラウンドエイリアスで解決するのが良さそうです。
まとめ
クラスメソッドがどこに定義されているのか?という問いに答えてくれる章でしたね。 ソースコードを読んでextendってなんだ?となることが多かったので、extendで特異クラスにメソッドを定義できるのは知っておきたいところ。 また特異クラスには特異クラスがあるので特異クラスの特異クラスの特異クラスとずっとつなげて行けるのでは…
メタプログラミングRuby 四章 ブロックの読書録
はじめに
前職の出勤が16日に終わり、しばしニートしております。 最近discordbというボイスチャットアプリのbot作成のラッパーGemが面白くていじっているんですが、このGemを読んでいるとメタプログラミングRubyの4章にこんなのあったなぁという気持ちが強まり、読書録として少し理解を文章に起こしてみました。5章と6章も個人的に何度でも反芻したいので書いていきたい。
ブロックは強力なツール
この章ではブロックがスコープを操る強力なツールだということを解説してくれています。 あと途中で上司が逃げます
ブロックは束縛を包んでくれる
Rubyではdef、class、module (これらはスコープを開く門)でスコープが切り替わってローカル変数なんかは共有できなくなります。
class Hogeclass class_value = 1 def hoge_method p class_value # => Error! end end
裏を返すとこの門を開かなければ新しいスコープは開かれません。 class やmoduleであればClass.new {}やModule.new {}としたり、 defをdefine_method { }にしてブロックを渡すことで新しいスコープが開かれなくなります。
class Hogeclass class_value = 1 define_method :hoge_method do p class_value # => 1 end end
このようにブロックは現在の束縛を包み込んでnewやdefine_methodといったメソッドに渡すことができます。
instance_eval
instance_evalはBasicObjectのインスタンスメソッドでブロックを渡すとレシーバーをselfにして評価してくれます。
class Hogeclass def initialize @foo = "value" end private def private_method p "秘密" end end hoge = Hogeclass.new hoge.instance_eval do @foo # => "value" end hoge.instance_eval do private_method # => "秘密" end
instance_evalを使えば例えばテストを書く際にクラスのインスタンス変数をいじったり、クリーンなオブジェクト(BasicObjectのインスタンス等)を作ってブロックを評価することができます。
呼び出し可能オブジェクト
ブロックの「コードを保管して、あとから実行する」という方式はなにもブロックだけのものではありません。
Procオブジェクト
ブロックはオブジェクトではないので後で呼び出したりしたい時に不便です。 ブロックをオブジェクトにするものとしてrubyにはProcクラスがあります。
double = Proc.new { |x| x * 2 } double.call(4) # => 8
わかりやすい変数にインスタンスをいれておけるので呼び出しも楽ですね。
ブロックをProcに変換するのに便利なメソッドもあります。lambdaとprocです。
double = lambda { |x| x * 2 } # => <Proc:0x00007f9e5~~~ triple = proc { |x| x * 3 } # => <Proc:0x00007f9e5~~
どちらもProcのインスタンスがつくられてるのがわかりますね。 Proc , proc とlamdaの違いとしてreturnの意味合いが違うことと、引数のチェックが違うことが挙げられます。 returnの意味合いが違うというのは以下のコードで確認できます。 (メタプログラミングRubyではProc.newとlamdaの違いしかなかったので今回はprocで試してみました)
def lambda_ten_double lambda_proc = lambda { return 10 } result = lambda_proc.call return result * 2 end def proc_ten_double proc_proc = proc { return 10 } result = proc_proc.call #ここでProcが定義されたスコープから戻ってしまう return result * 2 #ここまでたどり着かない end lambda_ten_double # => 20 proc_ten_double # => 10
引数のチェック方法にも違いがあり、
p = Proc.new { |a,b| [a,b] } l = lambda { |a,b| [a,b] } p.call(1, 2, 3) # => [1, 2] l.call(1, 2, 3) #=> ArgumentError (wrong number of arguments~
項数にも厳しくreturnの挙動も単なる終了なので特別な理由がなければlambdaを用いたほうが良さそうです。
&修飾でもブロックをProcに変換することができます。 また&修飾をつかえばProcオブジェクトをブロックに戻すこともできます。 どういうことかというと、
def block_method yield end def do_block_method(&block) #ここの&でブロックをProcオブジェクトに p block.class # => Proc &つけないとProcのまま! block_method(&block) # ここの&でProcオブジェクトからブロックに変換、 ないとオブジェクト渡してしまうのでArgumentエラー end do_block_method { p "hoge" } # => "hoge"
{ p "hoge" } が一度Procオブジェクトになり、またブロックにもどって最終的にblock_methodに渡されていることなります。
Methodオブジェクト
メソッドも呼び出し可能なオブジェクトです。 具体的には
class Hogeclass def initialize @x = "hoge" end def hogemethod @x end end hogeobject = Hogeclass.new method = hogeobject.method :hogemethod method.call # => "hoge"
メソッドオブジェクトは呼び出した時にオブジェクトのスコープで評価される。(今回はHogeclassクラス)
この束縛は解除して別のオブジェクトに束縛したりもできる。(ただし同じクラスかそのサブクラスのオブジェクトに限る)
class Fugaclass < Hogeclass def initialize @x = "fuga" end end unboundmethod = method.unbind fugaobject = Fugaclass.new rebindmethod = unboundmethod.bind(fugaobject) rebindmethod.call # => "fuga"
四章のクイズ
自分は4章最後のクイズでは、サンプルを見る前に以下のようなコードを書いていました。
def setup(&block) @setups << block end def event(description, &block) @setups.map(&:call) puts "ALERT: #{description}" if block.call end @setups = [] load 'events.rb'
eventごとに実行するならevent内でsetupに渡されたProcよびだしても良いのでは?と思って書きました。少し一つのメソッドでやりすぎかも。
この処理だけ行いたいならeventメソッドの 引数&blockは削除して、後置ifもif yieldにしても動作すると思いますが、ブロックを渡す必要があるメソッドでそんなことをしたらわかりにくすぎますね。
改めて見ると逃げた上司ののグローバル変数をどうして削除しない?が刺さるコードです。
グローバル変数を削除して、
lambda { setups = [] Kernel.send :define_method, :setup do |&block| setups << block end Kernel.send :define_method, :event do |description, &block| setups.map(&:call) puts "ALERT: #{description}" if block.call }.call load 'events.rb'
という形にしてみました。
おわりに
一度読んだ本をいざ文章にまとめてみるといろいろ考えることが多くていいですね。ガシガシ続けて行きたひ。 色んな人からツッコミもらいたいのでQiitaに書くか迷いましたが、このブログの主目的が「本の理解を文におこして再確認する」と考えていますし、先人の知見をQiitaに乗っけるのは違くない?と思ってブログに起こしました。