(Ruby) Hashの要素を非破壊的に削除する
こういう欲求に出くわすたびに一々調べて、思い出すのに割合時間を喰っている気がするので。 "Ruby"、"Hash"、"要素"、"非破壊的"、"削除"、でこの方法が引っかかる様にしたかったのが大きいです。
結論
reject
を使う。
[41] pry(main)> hash = {a: 1, b: 2, c: 3} => {:a=>1, :b=>2, :c=>3} [42] pry(main)> hash.reject {|key| key == :a} => {:b=>2, :c=>3} [43] pry(main)> hash => {:a=>1, :b=>2, :c=>3}
delete
は破壊的なので使いづらいですね。
ActiveSupportのHash#except
も手軽ですが、バニラなRubyだけで終わらせられると嬉しいこともあるので。
環境
[47] pry(main)> pry-version Pry version: 0.12.2 on Ruby 2.5.1.
関数的メソッドをメソッド引数として渡す
関数的メソッドを引数にとる関数の定義がしたかったのだが、調べてもなかなか丁度良い記事がなかったのでメモ代わりに。 関数的メソッドとは明示的なレシーバを持たないメソッドのこと。素直にグローバルスコープにメソッドを定義するとこれになる。
def test_func(a,b,c) p a + b + c end def test_func_func(func) func.call(1,2,3) end func = BasicObject.method(:test_func) test_func_func(func) # 6 # => 6
どうもにメソッドに引数としてメソッドを渡すときはObject#methodを使ってメソッドを一度オブジェクト化する見たいなのだが、Object#methodは呼び出し時にメソッドのレシーバを明示する必要がある。 では関数的メソッドのレシーバとして記述するオブジェクトは何が適切なのか、という話になるが、これについて明確にこれが良い、というものは分からなかった。 例には、取り敢えず余計な要素を持たないBasicObjectなんか適しているんじゃないだろうか、という推測のもとBasicObjectを用いている。 どなたかご存知でしたら教えてください。
20/2/3追記
id:ikngttyさんがコメントで下記のことを教えてくださいました。
- トップレベル(グローバルスコープ)に定義されたメソッドはObjectのprivateインスタンスメソッドとして定義される。
- トップレベルでの挙動は
self.test_func(args...)
に相当(selfはmainを指す)する。 - selfは省略可能なので、
method(:test_func)
でのオブジェクト化が可能。
ですので、上記を踏まえると以下の書き方が適切に思えます。
def test_func(a,b,c) p a + b + c end def test_func_func(func) func.call(1,2,3) end func = Object.new.method(:test_func) # メソッドの所属先を明示的に表記する場合 func = method(:test_func) # 簡潔に表記する場合 test_func_func(func)
id:ikngttyさんもおすすめされていますが、トップレベルでレシーバが省略されている時点Objectのメソッドであることは確定するので、selfを省略する書き方が個人的にもしっくりきます。メソッドの所属先を明示するためだけにObjectクラスのインスタンスを生成するのはかなり気が引けます。 また、こちらもid:ikngttyさんがおすすめされていますが、特にメソッドを使い回すなどしない場合であればラムダやProcが適切そうです。
いただいたコメントにとても丁寧な解説がありますので、そちらも是非ご覧下さい。
他参考:Ruby のトップレベルメソッドって結局なんなの - Qiita
20/2/25追記
id:ikngttyにいただいたコメントがMarkdown形式のものだったので、許可をいただき、コメントをそのままここに載せさせていただくことにしました。 大変丁寧な解説をありがとうございました。
以下コメントです。
取り敢えず余計な要素を持たないBasicObjectなんか適しているんじゃないだろうか、という推測のもとBasicObjectを用いている。
残念ながら、お望みの効果は得られていない状態になっています。
Object#method と Module#instance_method
Ruby には似たような役割を持つメソッドとして、Object#method
と Module#instance_method
があります。
# Object#method abs123 = -123.method(:abs) p abs123 # => #<Method: Integer#abs> p abs123.call # => 123 # Module#instance_method abs = Integer.instance_method(:abs) p abs # => #<UnboundMethod: Integer#abs> abs234 = abs.bind(-234) p abs234 # => #<Method: Integer#abs> p abs234.call # => 234
つまり、以下の2つが大体同じ関係になっています。
インスタンス.method(:name)
クラス.instance_method(:name)
これを混同して クラス.method(:name)
としてしまうと、以下のようになります。
abs = Integer.method(:abs) # => undefined method `abs' for class `#<Class:Integer>' (NameError)
本記事の
func = BasicObject.method(:test_func)
こちらも同様の構造です。
クラス.method(:name) では何が起きているのか
紛らわしい文章になりますが、クラスをインスタンスとして見た場合、それのクラスはClass
クラスです。
紛らわしいので、以下のコードを見た方がおそらく早いです。
class Foo end foo = Foo.new p foo.class # => Foo p Foo.class # => Class p BasicObject.class # => Class
なので BasicObject.method(:name)
は Class.instance_method(:name)
と意味合いが近いです。
p BasicObject.methods == Class.instance_methods # => true
これは BasicObject
に限らず全てのクラスについて言えます。
p Foo.methods == Class.instance_methods # => true
つまり、以下の2つのコードは同じということです。
func = BasicObject.method(:test_func)
func = Foo.method(:test_func)
この辺が「(BasicObject
を用いても)お望みの効果が得られてない」という根拠です。
「グローバルスコープに定義したメソッド」の正体
では改めて、何がレシーバーとして適切なのか。これは、「グローバルスコープに定義したメソッド」について詳しく調べることが近道となります。
main
オブジェクトについてのリファレンスに、この辺の挙動が詳しく書いてあります。
要約
トップレベルでの self を表すオブジェクトです。
main では参照できない事に注意してください。トップレベルで self から参照してください。
トップレベルで定義したメソッドは Object の private インスタンスメソッドと して定義されます。
(太字による強調はコメント主の私による)
例えば以下のコードで test_func
を呼べるのも、test_func
が「グローバルスコープ」だからなのではなく、 Foo
が Object
を継承してるから、というのが真相です。
def test_func(a,b,c) p a + b + c end class Foo def hoge test_func(2, 3, 4) end end Foo.new.hoge # => 9
また、BasicObject.method(:test_func)
がうまく動いたのも、Class
が Object
を継承しているからです。
余談ですが、p
や puts
などの多くの組み込み関数も似たような仕組みになっています。実はこれらは Kernel
クラスのメソッドとして定義されていて、 Object
はこの Kernel
を継承しています。なので、test_func
と同様に p
や puts
もいろんな場所から呼べるというわけです。
適切なレシーバーは何なのか
ここまでを踏まえてようやく本題です。
案1. BasicObject のインスタンス
とりあえず記事内での考えを延長して、以下のようなコードを考えたくなるかもしれません。しかしこれはエラーになります。
func = BasicObject.new.method(:test_func) # => undefined method `method' for #<BasicObject:0x00007f9c988641b0> (NoMethodError)
理由はもちろん、test_func
が Object
のメソッドだからであり、BasicObject
には含まれないからです。
案2. Object のインスタンス
じゃあ Object
なら動くのか。その通りです。
func = Object.new.method(:test_func) test_func_func(func) # => 6
案3. main オブジェクト(オススメ)
トップレベルで動かすのと全く同じ状態で渡したいなら、トップレベル(=main
オブジェクト)をレシーバーにするのが良いです。上で抜粋した通り、main
オブジェクトは self
で参照します。
func = self.method(:test_func) test_func_func(func) # => 6
なお、self
は省略できます。
func = method(:test_func) test_func_func(func) # => 6
なんと、「レシーバーなんて要らなかったんや!」というオチ。
案4. そもそもメソッドを使わない(一番オススメ)
レシーバーが必要なのはメソッドだからです。Proc
や lambda
を使えばレシーバー無しの「関数」が扱えます。
test_func = lambda { |a, b, c| p a + b + c } def test_func_func(func) func.call(1, 2, 3) end test_func_func(test_func) # => 6
ちなみに lambda
の代わりとして ->
という特殊なリテラル記法もあります。
test_func = ->(a, b, c) { p a + b + c }
以上、コメントで長々と書いてしまいましたが、参考になりましたら幸いです。
ブロック付きgsubの返り値の違いについて
基本的な話なのだがRubyのブロックには二通りの記法がある。
Objects.each { |object| object.method }
と
Objects.each do |object| object.method end
だ。
僕は、手続き上両記法は等価なものだと思っていたのだが、どうもそうでは無いらしい、ということが分かったのでメモとして残しておく。
違い
さて、どのようなケースで等価ではないかというと、下記のような時だ。
pry(main)> s = "hello, world" pry(main)> puts s.gsub(/\w+/) {|word| word.capitalize } Hello, World => nil
このコードをdo...endによる記法で書くと以下の様な挙動になる。
pry(main)> s = "hello, world" pry(main)> puts s.gsub(/\w+/) do |word| pry(main)* word.capitalize pry(main)* end #<Enumerator:0x000056474a99ef70> => nil
原因
原因は、{...}
とdo...end
の結合の強さの違いにある。
Ruby 2.4.0 リファレンスマニュアルには下記のようにある
{ ... } の方が do ... end ブロックよりも強く結合します。 次に例を挙げますが、このような違いが影響するコードは読み辛いので避けましょう:
foobar a, b do .. end # foobarの引数はa, bの値とブロック
foobar a, b { .. } # ブロックはメソッドbの引数、aの値とbの返り値とがfoobarの引数
つまり、
pry(main)> s = "hello, world" pry(main)> puts s.gsub(/\w+/) do |word| pry(main)* word.capitalize pry(main)* end #<Enumerator:0x000056474a99ef70> => nil
では、putsメソッドの引数はs.gsub(/\w+/)
とdo...end
ブロックと解釈されているのだ。
それぞれ、分けて実行してみると、以下のようになる
pry(main)> puts s.gsub(/\w+/) #<Enumerator:0x0000560d6a307a78> => nil
pry(main)> puts do pry(main)* s = 10 pry(main)* end => nil
なるほどこの2つが合わさっていたのか。
Rubyではブロックを取るメソッドをブロックを省略して書いた場合、Enumeratorオブジェクトを返す。 gsubの引数に正規表現パターンのみを与えた場合、ブロックを使っての置換の記法になるため、1つめはこれだろう。
2つめはなんだろうか。 チラッと調べてみた所、ブロックを取らないメソッド(内部にyieldがない)にブロックを渡した場合、ブロックは単に無視される、という話が引っかかった。 多分これなんじゃないだろうか。こっちについてはもう少し確認する必要がありそうだけれども。
バージョン
$ ruby --version ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-linux] => nil
参考
Ubuntu 16.04 LTSに最新のrbenvを入れた
概要
手持ちのUbuntu 16.04 LTSにaptでrbenvを入れたのだが、バージョンが古くて2.4系のrubyがインストールできなかった。 どうしても2.4系が使いたい気持ちだったため、purgeして野良ビルドを試みた。 その時の記録。
参考
ほぼ上記の内容の通りなので、英語が読めるならこっちを読めばいい。
rbenvのビルド
# リポジトリをクローンしてくる $ git clone https://github.com/rbenv/rbenv.git ~/.rbenv # rbenvを高速化するためにbash拡張を試みる。失敗しても別に気にしなくて良い $ cd ~/.rbenv && src/configure && make -C src # パスを通す。zshを使うなら~/.zshrcにリダイレクトする $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
以上で完了。下記を実行して、
$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash
下のような表示が出ればうまくビルドできている。
Checking for `rbenv' in PATH: /usr/local/bin/rbenv Checking for rbenv shims in PATH: OK Checking `rbenv install' support: bash: line 114: : command not found () Counting installed Ruby versions: none There aren't any Ruby versions installed under `~/.rbenv/versions'. You can install Ruby versions like so: rbenv install 2.2.4 Checking RubyGems settings: OK Auditing installed plugins: OK
ruby-buildのビルド
rbenv install
コマンドを使えるようにするために、ruby-buildをインストールする。
rbenvのプラグインとしてruby-buildをインストールする。
# プラグイン用ディレクトリの作成 $ mkdir -p "$(rbenv root)"/plugins # 先のディレクトリ内にリポジトリをクローンしてくる $ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
インストール可能なrubyバージョンの確認
$ rbenv install --list
で確認できる。後は、希望のバージョンを
rbenv install 2.4.2
でインストールすれば良い。
group_collection_selectメソッドでのエラー[Ruby on Rails 5 アプリケーションプログラミング]
Table of Contents
grouped_collection_selectメソッドでのエラー
最近「Ruby on rails 5 アプリケーションプログラミング」の本を読みながらrailsの勉強をしているのだが、本の記述と配布されているサンプルコード(2017/10/29時点)に誤りがあり、割と長い時間嵌ってしまった。 同じ問題に突き当たる人がいないとも限らないので、解決方法を置いて置く。
問題は「4.1.9 選択ボックスの選択をグループ化するーgrouped_collection_selectメソッド」で解説されているサンプルコード
<%= form_for(@review) do |f| %> レビュー対象書籍: <%= f.grouped_collection_select :book_id, @authors, :books, :name, :id, :title %> <% end %>
についてのものだ。 本の手順通りに進めてこのテンプレートファイルを記述し(もちろん対応するルート定義と、コントローラーにメソッドが用意されている必要がある)、サーバーを立ち上げてアクセスすると、
undefined method 'books' for #<Author:~~~>
といったようなエラーに出くわし、ページへのアクセスができない。 これは、この時点でauthorモデルとbookモデルの間にアソシエーションを定義していないことに起因している。配布されているサンプルプログラムの方も同様の理由でエラーを吐く。
ここで問題になっているアソシエーションとはなんだろうか。少し調べた内容を置いておく。
アソシエーション
データベースのテーブル間の関係をrailsのモデル間の関係に落とし込んでrails側から操作できる用にするためのもの。データベースの基本的な用語についてはリンク参照。 テーブルの一つのレコードに対して別のテーブルの一つのレコードが結びついている場合を1:1の関係、別のテーブルのn個のレコードが結びついている場合を1:nの関係などと呼んだりする。
どうも、grouped_collection_selectメソッドはこのアソシエーションの情報を元に対応するbookモデルのパラメータを取得するはずだったのだが、記述がなかったので叶わなかった、ということみたいだ。
対処
そんなわけで、欠けているアソシエーションの情報を補ってやれば解決するということが分かった。今回実装したいauthorとbookの関係性は複数:複数の関係性で、このようなものは中間テーブルを介した表現が一般的らしい。 メソッドとしてはhas_and_belongs_to_manyを使い、下記の用になる。
.../railsbook/model/app/models/ 以下のbook.rbとauthor.rbにアソシエーションを記述する
class Book < ApplicationRecord has_and_belongs_to_many :authors #authorとのアソシエーションを追加 end
class Author < ApplicationRecord belongs_to :user has_and_belongs_to_many :books #booksとのアソシエーションを追加 end
has_and_belongs_to_manyメソッドは複数対複数のアソシエーションをモデルに追加するメソッド。対応するテーブルの関係に上下をつけない為、双方のモデルに記述する必要がある。
以上の変更を加えれば、/view/group_selectのページにアクセスできるようになる。