ウェブエンジニア珍道中

日々の技術的に関する経験を書いていきます。脱線もしますが助けになれば幸いです。

RubyのProcについてまとめる

RubyのProcとかMethodとかの理解が少し進んできたのでまとめる

今回はProcについて

Procとは

Rubyのブロック文をオブジェクト化したもの

ブロック文はdo ~ end{ ~ }でよくつかう一連の処理をまとめたもの

だからといってdo ~ endを直接変数に入れるとかはできない。以下の方法で作成する

# 以下2つはエラー
hoge = do do_anything end
hoge = { do_anything }

# Proc.newを使う
print_proc = Proc.new { |msg| puts msg } 
# Kernel.procを使う
print_proc = proc { |msg| puts msg }
# Kernel.lambdaを使う
print_proc = lambda { |msg| puts msg }
# アロー演算子を使う
print_proc = ->(msg) { puts msg }

lambdaとProc.newでは引数でエラーが出るかどうかといった挙動が少し違ったりするがここでは割愛

使いみち

call[]で直接実行ができる

print_proc = lambda { |num| puts num }

print_proc.call(1)
=> 1
print_proc[1]
=> 1

あとはeachなどブロック文を受け取るメソッドにわたす

# 直接block文を渡す場合
[1, 2, 3, 4].each do |num|
  puts num
end

# procを作成して渡す場合
print_proc = lambda { |num| puts num }
[1, 2, 3, 4].each(&print_proc)

[1, 2, 3, 4].each(&print_proc)&はprocを渡すとブロックとして取り扱ってくれる役割がある。procをブロック文に展開してメソッドに渡すイメージ

# ↓に展開されて動作しているイメージ
[1, 2, 3, 4].each { |num| puts num }

each(&:hoge)は何をしてるのか

よく見る書き方(eachだと挙動がわかりにくいのでmapにした。nextは次の整数を返すメソッド)

[1, 2, 3].map { |num| num.next }
=> [2, 3, 4]

# 上記のような処理の場合、こう書くのが一般的
[1, 2, 3].map(&:next)
=> [2, 3, 4]

「各要素に対してメソッドを一つだけ実行する時のイディオム」位のノリで覚えてたが実際はもっと複雑だった

map(&:next) の&部分はprocを受け取るとブロックとして解釈させるのはさっきと同じ。今回違うのはここに:nextというsymbolを渡しているというところ。

map(&proc)

map(&:next)

実はこの&にはまだ役割があり、symbolを渡された時には暗黙的にto_procメソッドを実行し、procに変換した上で更にブロック文に展開を行う

流れは以下の通り
:next → (:next.to_procを実行) → proc → ブロック文

この手順が(&:next)に含まれている。めっちゃ隠れて色々やってる

to_procの役割

docs.ruby-lang.org

「受け取った引数に対して、そのシンボル名のメソッドを実行する」procを作成するイメージ(ちょっと語弊ありそう

nextメソッドの場合

next_proc = :next.to_proc
next_proc.call(1)
=> 2

受け取った数字に対して.nextを実行するprocを作成している、挙動は以下と同じ

next_proc = lambda { |num| num.next }
next_proc(1)
=> 2

総合するとmap(&:next)は「 :nextにこっそりto_procを叩くことで、【引数に対してnextを実行する】procを作成して、ブロック文と解釈してmapにわたす」ということをしている

引数がいる場合

+など引数が必要なメソッドは第2引数に入れる

add_proc = :+.to_proc

add_proc.call(1, 4)
=> 5
add_proc[1, 9]
=> 10

Rubyのmoduleでサービスクラスを作る

Rubyにおけるサービスクラスの作り方で、一つ作り方を考えたので残しておきます。

サービスクラスとは複数のモデルを使ってビジネスロジックを記述を行うものです、ここでの詳細は割愛しますが、以下の記事が有名でとても勉強になります。

qiita.com

例として「AとBを行うHogeService」を作ります。

ソース

実際のソースがこんな感じ。

module HogeService
  def execute(hoge, fuga)
    a(hoge)
    b(fuga)
  end

  def a(hoge)
    puts "execute a action"
  end

  def b(fuga)
    puts "execute b action"
  end

  # ↓によってHogeService.executeの形で呼べる
  module_function :execute, :a, :b
  # プライベートメソッド化
  private_class_method :a, :b
end

よく見る例として moduleではなく class内で以下のように定義しているものがあります。

class HogeService
  def self.execute(hoge, fuga)
    a(hoge)
    b(fuga)
  end
end

これでも良いのですが newでインスタンス作成できてしまったり self.executeという形があまり好きじゃなかったりするのでmoduleを使って作成しました。

module内で定義したメソッドはプライベートメソッドとして定義されるため他のクラスでincludeしないと使えませんが、 module_functionメソッドにメソッド名を渡すことで、そのメソッドがクラスの特異メソッドになります。要は executeがクラスメソッドになって、外側から使えるようになります。

あとはコントローラー等から叩いてやればOKです。

class AnyController
  def create
    @any_value = HogeService.execute(params[:a], @account)
  end
end

Rubyで再帰関数について軽くまとめ

再帰関数について軽くまとめてみます、今回はRubyで書きます。

再帰関数とは

関数Aの中で同じ関数Aを使う処理を行う関数のことです。

例:

def A(num)
  A(5)
end

↑みたいに普通に呼び出すだけだと、Aが呼んだAが呼んだAが呼んだAが…と無限ループになってメモリを食い潰します。

よくある例は階乗を求める関数です。

def kaijo(num)
  return 1 if num.zero?
  num * kaijo(num - 1)
end

# 三項演算子ver
def kaijo(num)
  num.zero? ? 1 : num * kaijo(num - 1)
end

kaijo(3)とした場合、kaijo(2)、kaijo(1)と呼ばれていき、最終的にkaijo(0)が呼び出され、1が返却されることで再帰は終わります。よって無限ループは起こりません。

使い時

使い所が難しいのですが、階層になっているデータを扱うときに用いると便利なようです。主に木構造と呼ばれるものです。

というわけで試しに配列の入れ子を作ってみます。

[1, [1, 2, [6, 3, 2], 2], 3, [2, 4]]

で、これらをすべて足し合わせてみます。

分岐で作った場合

まずは無理やり分岐で(悪い例です)

def nested_array_sum(arrayOrNum)
  sum = 0
  arrayOrNum.each do |tmp|
    if tmp.instance_of?(Integer)
      sum += tmp
    elsif tmp.instance_of?(Array)
      arrayOrNum.each do |tmp|
        if tmp.instance_of?(Integer)
          sum += tmp
        elsif tmp.instance_of?(Array)
          arrayOrNum.each do |tmp|
            sum += tmp
          end
        end
      end
    end
  end
  sum
end

puts  nested_array_sum([1, [1, 2, [6, 3, 2], 2], 3, [2, 4]])
=> 26

データのネストの階層がソースのネストの階層と連動するようになってしまいました。とてつもなく読みにくい上に、これだとデータの階層が深くなるたびにこの関数を修正する必要があります。

再帰関数で作った場合

では次は再帰関数で。

def nested_array_sum(array_or_num)
  if array_or_num.instance_of?(Array)
    array_or_num.reduce(0) do |sum, tmp|
      sum + nested_array_sum(tmp)
    end
  else
    array_or_num
  end
end

puts array_sum([1, [1, 2, [6, 3, 2], 2], 3, [2, 4]])
=> 26

引数が配列か数字か確認し、配列ならループを回しつつ再帰的に関数を呼ぶ構造になっています。

先程と比べてネストも浅くスッキリした上に、階層が深くなっても浅くても問題なく動くようになりました。

まとめ

今回は例として入れ子の配列を再帰関数で足してみました。デメリットとしてメモリの消費が大きいというのがよくあげられますが、数百階層とかにでもならない限りは大丈夫だと思います。

木構造のデータを触る際には再帰による処理も候補に入れつつ考えたいと思います。

おまけ

ちなみにRubyにはArray#flattenという配列を一次元にする便利メソッドがあります。

[1, [1, 2, [6, 3, 2], 2], 3, [2, 4]].flatten.sum
=> 26

上記のようにこれを使うと一撃でできるのですが、勉強にならないため再帰で作ってみました。

コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus)

コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus)

Railsで特定のModelだけ別DBを操作するようにしてみた

システム間連携等の事情で、別DBを作ってデータを流し込みたくなったので実装してみました。

手順

例としてBookモデルを作り、別DBを対象にするようにします。

環境はmysqlです。元々使っているDBはmain_database、新しく使うDBはsub_databaseという名前にしています。

対象となるDBと、テーブルを作っておく

別DBによりmigrationの管理外なので、予めスキーマ(DB)とテーブルを作っておきます。

CREATE DATABASE sub_database;

use sub_database;

CREATE TABLE `books` (
  `id`         int(11) NOT NULL AUTO_INCREMENT,
  `name`       varchar(255) NOT NULL,
  `author`     varchar(255) NOT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

config/database.ymlに別DB用の設定を書く

元々の設定をベースに新しい設定項目を増やします。

環境(stagingとか)にprefixを付ける形で名付けるとRails.env + 名前の要領で指定できるので環境毎に用意がしやすいです。

今回は同じmysql内の違うデータベースに保存するので元の設定をベースにdatabaseの欄だけ上書きしたものを用意します。

# 元々の設定
development: &development
  adapter: mysql2
  database: main_database
  encoding: utf8mb4
  charset: utf8mb4
  username: test_user
  password: any_password
  host: localhost

# 追加する
another_db_development:
  <<: *default
  database: sub_database

モデルに別DB用の設定を使うように指定する

他のモデルと同様app/modelに定義します。

booksテーブルの場合Bookクラスといった要領で単数系→アッパーキャメルケースにしてあげましょう。勝手にテーブルを見つけてくれます。ここは元のRailsのお作法通りです。

class Book
  establish_connection("another_db_#{Rails.env}".to_sym)
end

establish_connectionconfig/database.ymlで作った設定を指定することで別のDBを参照するようにしています。

使ってみる

普通のmodelを使うときと同じノリでOKです。

Book.create(name: "こころ", author: "夏目漱石")

Book.first # ↑で作ったものが取れる

別DBなのでjoinは動きませんが、手軽にアクセスができる環境ができるのでRailsはすげーなーと改めて思いました。

migrationが別管理なので、今後の課題です。(別PJによる管理とかなら問題なさそう)

immutableなクラスでto_hashを作るまで

Rubyにてセッタをもたないimmutableなクラスを作り、to_hashで値を取ってくるようにしたのでまとめます。

今回は例として本(Book)クラスを作ります。

完成品

class Book
  ATTRIBUTE_NAMES = [:name, :author, :price].freeze
  attr_reader *ATTRIBUTE_NAMES

  def initialize(name, author, price)
    @name = name
    @author = author
    @price = price
  end

  def to_hash
    ATTRIBUTE_NAMES.inject({}) do |hash, attribute_name|
      hash.merge({ attribute_name => send(attribute_name) })
    end
  end

  alias_method :to_h, :to_hash
end

puts Book.new("こころ", "夏目漱石", 1980).to_hash
# {:name=>"こころ", :author=>"夏目漱石", :price=>100}

attr_readerでゲッタをまとめて定義しています。attr_accessorとの違いはセッタの有無です。セッタを無くしimmutableに近づけています。

本来は

attr_reader :name, :author, :price

のように指定するのですが、直接指定すると後ほど指定した:name, :author, :priceの3つの名前を手軽に取ってくることができなかったため、まずクラスの定数に入れています。

to_hashメソッドでは先程のクラスの定数ATTRIBUTE_NAMESを元にハッシュを作成しています。

def to_hash
  ATTRIBUTE_NAMES.inject({}) do |hash, attribute_name|
    hash.merge({ attribute_name => send(attribute_name) })
  end
end

keyを:nameなどの属性名のシンボルで、valueにnameなどのゲッタメソッドを叩いて取ってきた値を入れたハッシュを作って返しています。sendを使わない場合は以下でも動きます。

def to_hash
  ATTRIBUTE_NAMES.inject({}) do |hash, attribute_name|
    hash.merge({
      attribute_name => instance_variable_get("@#{attribute_name}") 
    })
  end
end

あと一応to_hでも大丈夫なようにエイリアスを貼りました。

DDDにおけるドメインオブジェクトとかはこんな感じで作っていったらなと思いました。また何かあればアップデートしていきます。

injectを初めてまともに使えた気がする…。