12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rails mapメソッドがふんわりしてたので調べてみた

Last updated at Posted at 2022-07-28

はじめに

現場に入るとコードの至るところに扱い慣れていないメソッドが登場します。
これまでの記事でもpluckメソッドを紹介しました。
https://qiita.com/tatsu0209/items/61bdbe46216991aeb8d3
でも、現場でいちばん目にしたメソッドってなんだろうか・・・
そう、それはおそらくmapメソッドだ。
なんとなくわかったふりでふんわりと読んでいたので色々と調べてみました。
その結果を備忘録として残したいと思います。
もしもおかしな点やお気づきの点がありましたらご指摘いただけると大変ありがたいです。

環境(自分のPC)

Ruby 2.7.1
Rails 6.0.5

mapメソッドとは

公式ドキュメントでは次のように説明されています。

各要素に対してブロックを評価した結果を全て含む配列を返します。ブロックを省略した場合は Enumerator を返します。

簡単に言うと、要素を順番に取ってきて、指定した処理をしてくれるメソッドです。
記述方法としては次のようになります。

配列.map { |変数| 実行する処理 }

もしも実行する処理が複数行になるようであれば

配列.map do |変数|
  実行する処理
end

と記述します。
文字だけではわからないので今回も実際にやってみることにしましょう!

前提

いつものように、 countryモデルを作成し、値を準備します。

  • seeds.rbに次のように記述し、rails db:seedでDBにデータを登録します。
seeds.rb
Country.create!(
  [
    {
      name: '日本',
      capital: '東京',
      food: '寿司',
      language: "日本語",
      sports: "相撲"
    },
    {
      name: 'アメリカ',
      capital: 'ワシントン',
      food: 'ハンバーガー',
      language: "英語",
      sports: "野球"
    },
    {
      name: '秦',
      capital: '咸陽(かんよう)',
      food: '北京ダッグ',
      language: "中国語",
      sports: "戦い"
    },
    {
      name: 'ジュラ・テンペスト連邦国',
      capital: 'リムル',
      food: '肉',
      language: "日本語",
      sports: "戦い"  
    }
   ]
  )

次のようにデータベースに値が登録されています。
Image from Gyazo

いろいろやってみる

各要素の値に処理をし値を取得

Country.all.map{|country| country.name.length }

上のように記述することでcountry.nameの文字数を取得しています。
実行してみましょう。

pry(main)> Country.all.map{|country| country.name.length }
  Country Load (0.8ms)  SELECT `countries`.* FROM `countries`
=> [2, 4, 1, 12]

無事にcountry.nameの文字数を取得できました。
このようにmapメソッドを使用すると必ず配列で値が返ってきます。

各要素の値に処理をし値を取得(複数行)

Country.all.map do |country|
  if country.name.length >= 3 
    country.name.length
  end
end

複数行にまたがる処理は、上のようにdo〜endの記述方法が見やすいです。
今回はcountry.nameの文字数が3以上の要素に処理をするように記述しています。
実行してみます。

pry(main)> Country.all.map do |country|
  if country.name.length >= 3 
    country.name.length
  end
  Country Load (0.4ms)  SELECT `countries`.* FROM `countries`
=> [nil, 4, nil, 12]

実行されました。
今回はif文の条件式に合致しなかった要素はnilになっています。

ハッシュに対してmapメソッドを使う

ハッシュの場合にもmapメソッドを使うことができます。
この場合はkeyとvalueに分けてそれぞれに処理をするように記述します。
まずハッシュを準備します。

capital_food_hash = Country.pluck(:capital, :food).to_h

次のようにkeyがcapital、valueがfoodのハッシュを作りました。

{"東京"=>"寿司", "ワシントン"=>"ハンバーガー", "咸陽(かんよう)"=>"北京ダッグ", "リムル"=>"肉"}

そのハッシュに対してmapメソッドを実行してみます。
今回はvalueであるfoodの文字数を取得しています。
次のように記述します。

capital_food_hash.map {|k, v| [k, v + "美味しい"]}

kがkey、vがvalueとし今回はvに"美味しい"という文字列を追加しています。
実行してみましょう。

pry(main)>  capital_food_hash.map {|k, v| [k, v + "美味しい"]}
=> [["東京", "寿司美味しい"], ["ワシントン", "ハンバーガー美味しい"], ["咸陽(かんよう)", "北京ダッグ美味しい"], ["リムル", "肉美味しい"]]

無事取得できました.繰り返しになりますがmapメソッドは配列で値を返します。
もしもハッシュで値を返して欲しい場合は

capital_food_hash.map {|k, v| [k, v + "美味しい"]}.to_h

上のようにto_hメソッドをつけてハッシュにしましょう。

pry(main)>  capital_food_hash.map {|k, v| [k, v + "美味しい"]}.to_h
=> {"東京"=>"寿司美味しい", "ワシントン"=>"ハンバーガー美味しい", "咸陽(かんよう)"=>"北京ダッグ美味しい", "リムル"=>"肉美味しい"}

無事にハッシュとして値を取得できました。

ハッシュの場合も同じで複数行の場合は次のように記述します。
keyのcapitalの文字数が3文字以上であれば頭に「首都」と言う文字列をつけていく処理です。

capital_food_hash.map do  |k, v|
  if k.length >= 3
    [ "首都" + k, v ]
  end 
end

実行してみます。

pry(main)> capital_food_hash.map do  |k, v|
  if k.length >= 3
    [ "首都" + k, v ]
  end 
=> [nil, ["首都ワシントン", "ハンバーガー"], ["首都咸陽(かんよう)", "北京ダッグ"], ["首都リムル", "肉"]]

このように配列が返ってきます。条件式に合致しなければ配列内はnilになリました。

&:で省略する記法

mapメソッドは次の条件を満たせば、&:を使い省略して記述できます。
その場合以下条件全てに合致していなければなりません。

  • ブロックの引数が1つであること
  • ブロックで呼び出すメソッドに引数がないこと
  • ブロック引数に対して、メソッドを呼び出すこと以外の処理がないこと

では実際にやってみます。
今回もまず配列を準備します。

language = Country.pluck(:language)

次のような配列になります。

["日本語", "英語", "中国語", "日本語"]

この配列に対して文字数を取得するmapメソッドを使う場合は通常次のように書きます。

 language.map{|l| l.length}

実行してみましょう。

pry(main)> language.map{|l| l.length}
=> [3, 2, 3, 3]

この記述の場合

  • ブロックの引数が1つであること
  • ブロックで呼び出すメソッドに引数がないこと
  • ブロック引数に対して、メソッドを呼び出すこと以外の処理がないこと

の条件に合致しているので次のように省略できます。

language.map(&:length)

上のように省略して記述することができます。
実行してみます。

pry(main)> language.map(&:length)
=> [3, 2, 3, 3]

もちろん実行結果も同じです。

map!メソッドを使う場合

mapメソッドは元の値に対して影響を与えないのに対し、 map!は元の値を書き換えます。
元の値を違うものにしてしまうのですね。やってみましょう。
配列を準備します。

sports = Country.pluck(:sports)
  • mapメソッドの挙動
Country.pluck(:sports)
pry(main)> sports
=> ["相撲", "野球", "戦い", "戦い"]

この配列に対してmapメソッドで各要素の文字数を取得してみます。

sports.map{|s| s.length}

実行してみます。

pry(main)> sports.map{|s| s.length}
=> [2, 2, 2, 2]

無事実行されました。
実行された後sportsの値を見てます。

pry(main)> sports
=> ["相撲", "野球", "戦い", "戦い"]

配列の要素は元の値のままです。

  • map!メソッドの挙動
    同じようにmap!メソッドでもやってみます。
sports.map!{|s| s.length}

実行してみます。

pry(main)> sports.map!{|s| s.length}
=> [2, 2, 2, 2]

ここまでは同じですね。
実行された後sportsの値を見てます。

pry(main)> sports
=> [2, 2, 2, 2]

変わっちゃってる!そういうことです!

eachメソッドとの違い

私は
「おいおい待ってくれ!
結局mapメソッドってeachメソッドと同じようなものじゃない!?」
と思いながら過ごしていました。
今回調べてみて結論似て非なるものだと思いました。

ということでmapメソッドとeachメソッドの違いを調べてみます。
mapメソッドを使用して、全てのcountryのnameの文字数を取得します。
mapメソッドで記述すると次のようになります。

map_countries = Country.all.map do |country|
    country.name.length
  end

次のような挙動になりました。

pry(main)> map_countries = Country.all.map do |country|
  country.name.length
  Country Load (0.5ms)  SELECT `countries`.* FROM `countries`
=> [2, 4, 1, 12]

戻り値はcoutryの文字数が配列として返ってきました。

  • eachメソッドを使う場合
    例えばeachメソッドでmapメソッドと同じような挙動を再現すると次のようになります。
each_countries = []
Country.all.each do |country|
  each_countries << country.name.length
end

よく見るとeachメソッドで記述したものはeach_countries = []を別に定義しています。
そして、定義したeach_countriesの配列に繰返しの処理で取得した値を代入していく形になります。
動きとしては次のとおりです。

pry(main)> each_countries = []
Country.all.each do |country|
  each_countries << country.name.length
  Country Load (0.4ms)  SELECT `countries`.* FROM `countries`
=> [#<Country:0x00007ff0481f3598
  id: 1,
  name: "日本",
  capital: "東京",
  food: "寿司",
  language: "日本語",
  language: "日本語",
  sports: "相撲",
  created_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00,
  updated_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00>,
 #<Country:0x00007ff0481f3020
  id: 2,
  name: "アメリカ",
  capital: "ワシントン",
  food: "ハンバーガー",
  language: "英語",
  sports: "野球",
  created_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00,
  updated_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00>,
 #<Country:0x00007ff0481f2ad0
  id: 3,
  name: "秦",
  capital: "咸陽(かんよう)",
  food: "北京ダッグ",
  language: "中国語",
  sports: "戦い",
  created_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00,
  updated_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00>,
 #<Country:0x00007ff0481f28a0
  id: 4,
  name: "ジュラ・テンペスト連邦国",
  capital: "リムル",
  food: "肉",
  language: "日本語",
  sports: "戦い",
  created_at: Mon, 25 Jul 2022 21:00:23 UTC +00:00,

戻り値はmapメソッドの場合は処理後のものであったのに対し、eachメソッドはCountry.allの値となっています。

結果わかったことは次の通りです。

  • mapメソッドは戻り値に処理後の値を返し、eachメソッドは処理前の値を返す。
  • eachメソッドよりもmapメソッドの方が記述量が少ない。また、mapメソッドはeachメソッドに比べてブロックの中に処理の本質のみを書くことができる。

※ここでは、eachメソッドよりmapメソッドの方が良いと言うわけではなく、mapメソッドのメリットを記述しています。
状況に応じて使い分けましょう。(使い分けはこれから勉強します!)

おわりに

mapメソッドってこんなに色々な使い方があったんだ〜と納得しました。
何よりも実務の中では&:の省略記法で意味不明になっていたので理解できてよかったです。
実戦でも活かしていければいいなと思います。
ありがとうございました。

参考記事

参考文献

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?