🦁

Rubyの基礎文法

2023/04/06に公開

こんばんは。
よくわからなかったRubyの基礎となる文法をまとめました。
なにか間違いがある場合、ご指摘いただければ幸いです。
何卒、よろしくお願い申し上げます!

環境

Ruby 3.1.2
Ruby on Browserでも動作確認済みです。
https://rubyonbrowser.ongaeshi.me/

引数の受け方(name, *args, &block)

*argsは全ての引数を配列で受け取る。&blockはブロックを引数で受け取る

def hoge(name, *args, &block)
  p "#{name} #{args} #{block}"
  p args[0]
  p args.last
end

x = [1,2,3].map{|v| v ** 2}

hoge("qiita",  1, 2, 3,  x)

#=> "qiita [1, 2, 3, [1, 4, 9]] "
#=> 1
#=> [1, 4, 9]

# 名前は別になんでもいい
def hoge(name, *baz, &foo)
  p "#{name} #{baz} #{foo}"
  p baz[0]
  p baz.last
end

x = [1,2,3].map{|v| v ** 2}

hoge("qiita",  1, 2, 3,  x)

#=> "qiita [1, 2, 3, [1, 4, 9]] "
#=> 1
#=> [1, 4, 9]

**を使う

#キーワード引数を用いて引数の値を明確にしつつ、**を用いることで、
#キーワード引数以外の該当しない全ての引数をハッシュとして受け取ることができる
def information(price, drink: true, food: true, **others)
    puts others #=> {:location=>"Tokyo", :tables=>10}
end

information(1000, drink: true, food: true, location: "Tokyo", tables: 10)

# **はハッシュの展開もできる。
# paramsに格納されているハッシュをinformationの引数に渡しつつそこで展開して、
# informationの**othersでキーワード引数以外のハッシュを全て受け取っている
def information(price, drink: true, food: true, **others)
    p others #=>{:location=>"Tokyo", :tables=>10, :card=>true}
    p others[:tables] #=>10
end 

params = {location: "Tokyo", tables: 10, card:  true}

information(1000,  drink: true, food: true,  **params)

*を使う

*と..の範囲式を組み合わせて、今の時間が夜の21時から朝の5時までに当てはまるかを真偽値で返すメソッド

def stay_time?(time)
  [*21..23, *0..5].include?(time)
end
#[21, 22, 23, 0, 1, 2, 3, 4, 5].include?(time)と同じ

演算子の再定義 def ==(other)

Rubyではメソッドとして定義されている演算子もある。そのためそれら演算子を自分で再定義(オーバーライド)することも可能。

再定義できる演算子(メソッド)

`|  ^  &  <=>  ==  ===  =~  >   >=  <   <=   <<  >>
+  -  *  /    %   **   ~   +@  -@  []  []=  ` ! != !~`

再定義できない演算子(制御構造)

`=  ?:  ..  ...  not  &&  and  ||  or  ::`

再定義できない=は変数への代入として使う hoge = Hoge.newのこと。

セッターメソッドとして使う=は再定義可能。

一意な店舗コードと店舗名を属性に持っているオブジェクトに対して、店舗コードが同一ならtrueを返したいとき。

通常の==だと下記のようにobject_idが一致した場合にtrueを返す。

class Shop
  attr_reader :code, :name
  
  def initialize(code, name)
    @code = code
    @name = name
  end
end

x = Shop.new("qawsed",  "zenn")
y = Shop.new("rftgyh",  "zenn")
z = Shop.new("qawsed",  "zenn")

# xとzは同一コードなので期待しているのはtrueだが、
# デフォルトの==演算子はobject_idが一致した場合にのみtrueを返すようになっている。
puts x == y #=> false
puts x == z #=> false

x = Shop.new("qawsed",  "zenn")
a = x

puts x == a #=> true 

そこで同一コードの場合にtrueを返却するように「==」を再定義する

class Shop
  attr_reader :code, :name
  
  def initialize(code, name)
    @code = code
    @name = name
  end

  def ==(other)
    other.is_a?(Shop) && code == other.code
    #比較対象のotherがShopかつ、Shopのcodeとotherのcodeが一致した場合に
    #trueを返すように再定義
  end
end

x = Shop.new("qawsed",  "キータ")
y = Shop.new("rftgyh",  "キータ")
z = Shop.new("qawsed",  "キータ")

puts x == y #=> false
puts x == z #=> true

「||=」を使ってメモ化

変数がnilもしくはfalseの時に値を挿入できるnilガード

hoge ||= 100 #hogeがnilまたはfalseのときに100が挿入される

このnilガードを使ったメモ化とは、重いAPIを処理する場合に用いる

下記は毎回 heavy API の呼び出しが発生してしまう。

class Shop
  def location
    heavy_data[:location]
  end
  
   def information
    heavy_data[:information]
   end

    def heavy_data
     #heavy APIからデータを取得する処理を記述
    end
end

そこでインスタンス変数と||=を用いてメモ化する

class Shop
   def location
       heavy_data[:location]
    end

    def information
       heavy_data[:information]
    end

    def heavy_data
       @heavy_data ||= begin
       #heavy APIからデータを取得する処理を記述
    end
end

上記によって、heavy_dataメソッドを始めて呼び出したときだけ、heavy APIからデータを取得する。以降heavy_dataメソッドを呼び出すと、@heavy_dataに格納された値が再利用される。

「||=」を使って遅延初期化

遅延初期化とはinitializeメソッドで初期化をせずに、インスタンス変数に初めてアクセスした際に初期化されること。

#遅延初期化を行わない場合
#Shop.newをするだけで重い処理が走ってしまう
class Shop
  attr_reader :information

  def initialize
    @information = #何か重い処理でinformationを初期化する
  end
end

#遅延初期化を行う場合
#informationメソッドを呼びださない限り重い初期化処理が走らない
class Shop
  # def initialize
  #   initializeでは何もしない
  # end
  def information
    @information ||= #重い処理でinformationを初期化
  end
end

しかし代入する値がnilやfalseの可能性のあるオブジェクトに ||= をすると毎回nilやfalseが代入されてしまうので注意が必要

述語メソッドと破壊的か非破壊的か

Rubyでは末尾に!や?がついているメソッドがある。bool値で返すので述語メソッドと呼ばれている。

また、自分でも末尾に?をつけたメソッドを定義できる。

# ? は真偽を判定してboolで返す
"".empty? #=>true
"a".empty? #=>false

# 自前で実装できる
# 3で割れたらtrue,それ以外の数値はfalseを返す
# nilや配列など割れない値に対して実行された場合はエラーになる
def three?(n)
  n % 3 == 0
end

x = "hoge"
y = x.upcase
p x #=> "hoge"
p y #=> "HOGE"

# !は破壊的なメソッドの可能性がある
x = "hoge"
y = x.upcase!
p x #=> "HOGE"
p y #=> "HOGE"

!がつかないメソッドにも破壊的なメソッドがある。例えばconcatなど。

!がつくメソッドは!がつかないメソッドよりかは危険。

[@yukihiro_matz]
https://twitter.com/yukihiro_matz
 Rubyの「!」つきメソッドは、「!」がついていないメソッドよりもより「危険」という意味です。
更に exit! のようにレシーバーを破壊しないが(exitより)危険というメソッドもあります。

!!を使って必ずtrueかfalseを返す

Rubyではfalseとnilは偽。それ以外は真と評価される。

#値がnilならnilを返す
def shop_exists?
  shop = nil
end
p shop_exists? #=> nil

def shop_exists?
  shop = find_shop #=>何かDBからfoodを探す処理。なければnilになる
  if shop #=>上記のfoodがあったらtrue。それ以外がfalse
    true
  else
    false
  end
end

#省略して書ける
def food_exists?
  !!find_food #=>!!を前につけることでtrueかfalseを必ず返す。
  #つまりDBからfoodを探しつつ、あったらtrueでnilならfalseを返す
end

&:メソッド名を使った省略形

この省略形を使える条件としては、

  • ブロックパラメータが一つであること
  • そのブロックパラメータに対して一つしかメソッドを呼び出してないこと
  • そのメソッドに引数がないこと
["food", "drink"].map{ |x| x.upcase } #=> ["FOOD", "DRINK"]

#省略形を使った場合
["food", "drink"].map(&:upcase) #=> ["FOOD", "DRINK"]

#&:を使った省略が使えない場合とは?
#引数が存在する場合
["food", "drink"].map { |x| x.include?("food") } #=> [true, false]
["food", "drink"].map(&:include?("food")) #=> SyntaxError

#メソッド以外の演算子を用いる場合
[100, 1000].map { |x| x + 1} #=> [101, 1001]
[100, 1000].map(&:x + 1) #=> SyntaxError

#複数の文になる場合
[100, 1000].map do |x|
  s = x +1
  s.to_s
end #=> ["101", "1001"]

[100, 1000].map(&:s = x + 1 s.to_s) #=> SyntaxError

定数について

大文字から始まる定数に値を代入できる。

class Shop
  DEFAULT_PRICE = 100
end
#クラス外部から参照できる
Shop::DEFAULT_PRICE #=>100

#pricate_constantでクラス外部から参照できなくできる
class Shop
  DEFAULT_PRICE = 100
  private_constant :DEFAULT_PRICE
end
Shop::DEFAULT_PRICE #=>private constant Shop::DEFAULT_PRICE referenced (NameError)

#メソッド内では定数は使えない
def food
  DEFAULT_PRICE = 1000
end
#=>dynamic constant assignment DEFAULT_PRICE = 1000(SyntaxError)

#定数にはメソッド,条件分岐,配列,ハッシュなど代入可能
class Shop
  DEFAULT_PRICE = {
    FOOD: 1000,
    DRINK: 200
  }
  DOUBLE_PRICE = [10,100].map{|n| n * 10}#=>[100, 1000]
end
Shop::FOOD #=>1000
Shop::DEFAULT_PRICE #=>{:FOOD=>1000, :DRINK=>200}
Shop::DOUBLE_PRICE  #=>#=>[100, 1000]

注意点としてRubyの定数は再代入が可能

class Shop
  DEFAULT_PRICE = 100
  DEFAULT_PRICE = 200 
end

Shop::DEFAULT_PRICE #=>200
Shop::DEFAULT_PRICE = 300 #=>300

定数の再代入の解決策としては以下がある

  • Shop.freezeでクラスそのものを変更できなくする
    • しかしクラス自体を変更できなくなるデメリットがある
  • Shopクラス内でfreezeを単体で記述
    • しかしその後のメソッドの定義ができなくなるデメリットがある

定数の再代入は明らかにみてわかるので上記の場合はfreezeする必要がない

しかしながら、ミュータブルなオブジェクトは定数であっても変更可能なので、ミュータブルなオブジェクトが入っている定数の場合はfreezeするべき。

ミュータブルかイミュータブルか

Rubyでは,数値,シンボル,真偽値,nilはイミュータブルで不変。

文字列,配列,ハッシュはミュータブルで変更可能。

つまり定数であってもミュータブルなら変更できる。

class Shop
  NAME = "qiita"
  MENU = ["rice", "fish", "beef"]
end
Shop::NAME.upcase! #=>"QIITA"
Shop::MENU << "foo" #=>["rice", "fish", "beef", "foo"]

上記のコードの場合は変更していることが分かりやすいが、

例えば、定数をあるメソッドの引数に渡して、その引数を使った処理を記述する場合には、変更を見落とすリスクが高まる。

そこで、freezeを使って不変にする

class Shop
  NAME = "qiita".freeze
  MENU = ["rice", "fish", "beef"].freeze
end
Shop::NAME.upcase! #=>`upcase!': can't modify frozen String: "qiita" (FrozenError)
Shop::MENU << "foo" #=>can't modify frozen Array: ["rice", "fish", "beef"] (FrozenError)

#それでも、配列の各要素は変更できてしまう
Shop::MENU[0].upcase! #=>["RICE", "fish", "beef"]

#配列やハッシュの各要素も全てfreezeしたい場合
#mapで各要素を展開してfreezeにしつつ、最後に返す配列自体もfreezeする
MENU = ["rice", "fish", "beef"].map(&:freeze).freeze
Shop::MENU[0].upcase!  #=>`upcase!': can't modify frozen String: "rice" (FrozenError)

キーワード引数

引数を**引数名: デフォルト値**で指定することによって、可読性が向上して、厳格になる。また、通常の引数は渡す順番に依存するがキーワード引数は順番に依存しない。

#通常は引数の順番に依存している
def buy_food(pizza, drink)
  "ピザの料金は#{pizza}円、ドリンクの料金は#{drink}円です"
end
buy_food(3000,500) #=>ピザの料金は3000円、ドリンクの料金は500円です

def buy_food(pizza, drink)
  "ピザの料金は#{pizza}円、ドリンクの料金は#{drink}円です"
end
buy_food(500, 3000) #=>ピザの料金は500円、ドリンクの料金は3000円です

#キーワード引数は順不同
def buy_food(pizza:, drink:)
  "ピザの料金は#{pizza}円、ドリンクの料金は#{drink}円です"
end
buy_food(drink: 500, pizza: 3000) #=>ピザの料金は3000円、ドリンクの料金は500円です

#デフォルト値があるキーワード引数
def buy_food(pizza: 2000, drink: 400)
  pizza + drink
end
buy_food #=> 2400

#デフォルト値のないキーワード引数
def buy_food(pizza:, drink:)
  pizza + drink
end
# キーワード引数に沿って値を指定
buy_food(pizza: 3500, drink: 500) #=>4000
# キーワード引数に沿って値を指定していないのでエラー
buy_food #=>`buy_food': missing keywords: :pizza, :drink (ArgumentError)

#想定外のキーワードも受け取りたい場合
def buy_food(pizza:, drink:, **hoge)
  puts pizza + drink
  puts hoge
end
buy_food(pizza: 3500, drink: 500, chicken: true, potate: true)
 #=>4000
 #=>{:chicken=>true, :potate=>true}

引数をnilガードで受け取る

# drinkを引数で指定しなかったときデフォルトでdrinkの値がnilになって、
# 最終的にnilガードで0が代入される
def buy_food(pizza:, drink: nil)
  drink ||=  0
  pizza + drink
end
buy_food(pizza: 3000) #=> 3000

# デフォルト値にnilを設定しない場合は、引数でdrinkを指定しないとエラーになる
def buy_food(pizza:, drink:)
  drink ||=  0
  pizza + drink
end
buy_food(pizza: 3000) #=>missing keyword: :drink (ArgumentError)

# デフォルト値指定だけだと、nilが渡された時にエラーになる
def buy_food(pizza:, drink: 0)
  pizza + drink
end
buy_food(pizza: 3000, drink: nil) #=> nil can't be coerced into Integer (TypeError)

Struct.new

dog = Struct.new(:name, :age)
pochi = dog.new("ぽち", 5)
puts pochi.age #=> 5
puts pochi[:name] #=> ぽち
puts pochi.weight #=> undefined method `weight'

どんな時に使うのか?

例えばホテルの料金に関するクラスについて

class HotelPrice
  attr_reader :price

  def initialize(price)
    @price = price
  end

  # 二次元配列からホテルのルーム料金とサービス料と消費税の合計を算出
  def total_cost
    price.map do |cost|
      ((cost[0] + cost[1]) * 1.1).floor
    end
  end
end

hotel_price_params = [[3980, 500],[4980, 600], [5980, 700]]
HotelPrice.new(hotel_price_params).total_cost #=> [4928, 6138, 7348]

上記のtotal_costメソッドは配列の構造に依存している。

つまり、cost[0]にルーム料金、cost[1]にサービス料金が格納されていることを知っている。

今は単純明快だが、total_costが様々なところで利用されているときに配列の構造が変わったら、色々なところで影響を受けてしまう。

配列の構造だけを知っているメソッドを抽出することで再利用性やメンテナンス性が高まる。

class HotelPrice
  attr_reader :price

  def initialize(price)
    @price = typed_cost(price)
  end

  # ホテルのルーム料金とサービス料と消費税の合計を算出
  def total_cost
    price.map do |cost|
      ((cost.room + cost.service) * 1.1).floor
    end
  end

  private
  
 #配列をサブクラス Cost のプロパティに当てはめる
  Cost = Struct.new(:room, :service)
  def typed_cost(price)
    price.map { |cost|
      Cost.new(cost[0], cost[1])}
  end
end

hotel_price_params = [[3980, 500],[4980, 600], [5980, 700]]
HotelPrice.new(hotel_price_params).total_cost #=> [4928, 6138, 7348]

上記の変更によって、total_costがなんのcostを算出しているのか分かりやすくなった。
そして、配列の構造の責務はtyped_costが請け負っている。
また、ロジックが増えてきたらStruct.newで作ったサブクラスをそのまま新しいクラスに移すことができる。

以上になります。
最後まで閲覧いただきありがとうございました!

Discussion