web-k.log

RubyやWebをメインに技術情報をのせていきます

Gitコマンドまとめ(2)~rebase/cherry-pick/revert/etc…~

| Comments

前回のGitコマンドまとめはローカルリポジトリ単独での利用やgithubで自分のリモートリポジトリだけで利用する場合等でもよく利用できそうなコマンドをまとめました。今回は複数人で平行開発する場合等でマスターリモートリポジトリを共有している際等に頻繁に利用するgit rebaseを中心に、使えそうなコマンドを紹介していきます。

コミット(ブランチ)を編集する: git rebase

複数人で利用するリモートブランチを扱う場合、masterブランチのみで開発を行うことは実質不可能となり、topicブランチやfeatureブランチ等、何かしらブランチを作成して品質を確保した上でメインブランチにPull Requestを行って反映するようなやり方をすることが多くなります。その際にメインブランチではコミットが頻繁に行われたりすると派生ブランチは古いバージョンのソースからブランチが切られていたりして、最新コミットに追従出来ていない状態においちることがあります。

Gitには平行開発を安全に、派生ブランチを健全に運用していくためのいくつかのコマンドがあり、その代表の1つとしてrebaseがあります。rebaseは単純に説明すると「既にコミット済みのものを再編集する」ものですが、最新コミットへのブランチの追従やコメントの付け替え、複数のコミットをまとめたり出来ます。

細かい使い方、ブランチの順番等を思い出す時はヘルプを参照するとブランチの図入りで例が載っているのでおすすめです:

1
$ git help rebase 

派生したブランチ(topic)にて開発していた場合に、メインブランチ(master)の開発が進み最新コミットが入り込んだ場合に追従し、最新コミットからのブランチから枝分かれするよう追従させる場合は以下を利用します

1
$ git rebase master  # topicブランチにてcheckoutしている状態でrebaseする

また、rebaseではありませんが、既にtopicブランチ自体も複数人で共有している場合等、ブランチにあるコミットを変更したくない(できない)場合にはmergeすることでも追従可能なので、ケースバイケースで使い分けます

1
$ git merge master   # 既存のコミットに追加する形でtopicブランチにマージコミットができる

rebaseを行うと枝元の古いコミットから順に最新コミットへの追従が行われていきますが、途中のコミットにてコンフリクトが発生する場合があります。コンフリクトが発生したコミットはリベース中の状態(無名ブランチ)となってコンフリクトの解消を手動で行うようにrebaseが中断します。そのときの対応コマンドは以下になります:

1
2
3
$ git rebase --continue # コンフリクトを手動で編集して解消出来た場合に修正を反映してコミットし、rebaseを再開する
$ git rebase --abort    # コンフリクト解消を止めて、rebase前の元の状態に戻す
$ git rebase --skip     # コンフリクトしたコミットを無視してrebaseを再開する

ブランチの派生元を別ブランチに差し替えたい場合は–ontoオプションを利用します

1
$ git rebase --onto master next topic # nextブランチから派生しているtopicブランチをmasterブランチの最新コミットから派生するブランチにrebaseする

過去のコミット編集を個別に行う場合は-iオプションを使います

1
$ git rebase -i HEAD~3 # チェックアウト中の過去3コミットについて個別に操作内容を指定する

コミットログと操作内容が書かれた文章がエディタで開かれる(最初はすべてpickが指定)ので、操作したいコミットを編集します。以下の操作がおこなえます

1
2
3
4
5
6
7
pick         コミット内容は編集せずそのまま反映
reword       コミットメッセージだけ編集する
edit         コミットを編集する
squash       一つ前のコミットと統合する(コミットログは残る)
fixup        一つ前のコミットと統合する(コミットログは統合先のもののみ残り、fixupコミットログは破棄される)
行を削除      コミットを削除する
行を入れ替え  コミット順を入れ替える

操作内容が決まったら編集を保存し、エディタを閉じるとrebaseが操作リストにしたがって行われます。

コミットした内容を取り消す: git revert

git resetを用いてもコミット内容は取り消せますが、痕跡も残らない強力なものなので、内容を取り消したという履歴を残すバージョン管理システムとして自然な取り消しを行いたい場合はrevertを用います

1
2
3
4
$ get revert HEAD~5                        # HEADから5つ前のコミットを取り消す
$ git revert ':/Commit Comment'            # 「Commit Comment」というコミットログのコミットを取り消す
$ git revert ':/Commit Comment' --no-edit  # 「Revert 'Commit Comment'」という定形文をコミットログに利用する
$ git revert -n HEAD~4                     # HEADから4つ前のコミットを取り消すが、コミットはせずに編集を続ける

特定のコミットをブランチに取り込む: git cherry-pick

pull-requestの一部の修正のみ取り込みたい場合や他のブランチから一部必要な機能が含まれているコミットのみを取り込みたい場合にcherry-pickを用います

1
2
$ git cherry-pick daf980923     # コミットdaf980923をチェックアウトブランチに取り込む
$ git cherry-pick -x jfa0032a2  # コミットjfa0032a2を元のコミット情報をコミットログに残した状態でブランチに取り込む

バグが入り込んだ位置を特定する: git bisect

コミットリストの二分検索により確認を行っていき効率的にバグ混入箇所を特定します。

1
2
3
$ git bisect start           # バグ特定開始
$ git bisect bad             # バグが混入しているコミットを指定
$ git bisect good daf980923  # コミットdaf980923はビルド(テスト)成功していることを知らせる

上記のコマンドを実行すると二分検索が始まり、中間のコミットがチェックアウトされる。後はビルド(テスト)を行って、

1
2
$ git bisect good  # ビルド(テスト)成功
$ git bisect bad   # ビルド(テスト)失敗

を繰り替えして最後に失敗したコミットがバグ混入箇所と特定できる。

1
$ git bisect reset  # バグ特定を終了し、元に戻す

他にもsubmoduleコマンド等色々有るが利用ケースが今のところないので割愛します。新しい有用なコマンド等が見つかった場合に続きを書きます。

Rails Internationalization (I18n) APIについて

| Comments

Ruby on Rails Guides: Rails Internationalization (I18n) API を自己解釈しながら翻訳していきます。 この記事は3章までの内容になります。

はじめに

I18n(internationalization) Gem はRails2.2から提供されており、多言語をサポートしたアプリケーションを提供するためのフレームワークである。 「国際化(I18n:internationalization)」を行うとは、全ての文字を抽象化し、日付や通貨などロケール(地域や言語)によるものをアプリケーションの外に出すことである。 「地域化(L10n:localization)」を行うとは、それらアプリケーションの外に出したものに対して、翻訳やフォーマットを提供することである。

I18n化するために必要なこと

  • I18nのサポートを確保すること
  • 辞書ファイルの場所をRailsに教えること
  • ロケールの設定/選択の仕方をRailsに教えること

L10n化するために必要なこと

  • Railsのデフォルトロケールの補完を行うこと
    • 例)日付/時間フォーマット、月の名前など
  • アプリケーション中に辞書のキーに相当する抽象化された文字列を設定すること
    • 例)フラッシュメッセージやView内の静的な文字列など
  • どこかに翻訳結果の辞書を保存しておくこと

公開 I18n API

最も重要なAPIが以下の2つ

1
2
3
4
translate # テキストの翻訳
localize  # 日付/時間をロケールに合わせたフォーマットに変換
I18n.t 'store.title' #translateをtに短縮したもの
I18n.l Time.now     #localizeをlに短縮したもの

Railsアプリケーションの初期化方法

Railsのデフォルトではconfig/localesフォルダにある.rbと.ymlファイルを自動でロードする。

1
2
en:
  hello: "hello world"

このサンプルの場合は、:enロケールだった場合は「hello」というキーを「hello world」という文字列にマップする。

デフォルトのappllcation.rbファイルに辞書の追加の仕方やデフォルトロケールの設定の仕方がコメントアウトで載っている。

1
2
3
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de

load_pathは辞書ファイルを自動ロードするための設定でRuby配列のパスになっている。 default_localeがデフォルトのロケール設定になる。無指定のときは英語がデフォルトロケールになる。

ロケールの設定と渡し方

複数のロケールを使う場合リクエスト中にロケール設定の仕方が必要になる。 ここで注意したいのは、sessionやcookieでロケールを保存して選択してはいけないことである。 ロケールはURLの一部に含めるべきで、URLを友人に送ったときは同じページで同じコンテンツで表示させるべきである。 RESTfulのアプローチでいけばこのルールから外れるべきではない。

簡単なロケールの設定の仕方は、ApplicationControllerのbefore_filterに以下のように書き、 URLのクエリとしてlocaleを渡す(例:http://localhost:3000?locale=de)方法である。

1
2
3
4
before_filter :set_locale
def set_locale
  I18n.locale = params[:locale] || I18n.default_locale
end

ドメイン名からロケールを設定

www.example.com なら英語、www.example.esならスペインのロケールというようにドメイン名でロケールを分ける。 トップレベルでロケールを設定すると以下のような利点がある。

  • ロケールがURLに含まれるのが明らかであること
  • 人々が直感的にコンテンツの言語を理解できること
  • 簡単な構成
  • サーチエンジンが異なる言語のコンテンツは異なるドメインのリンクとして扱うことを期待できること

ApplicationController以下のように記述すると実現できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
before_filter :set_locale
def set_locale
  I18n.locale = extract_locale_from_tld || I18n.default_locale
end
# Get locale from top-level domain or return nil if such locale is not available
# You have to put something like:
#   127.0.0.1 application.com
#   127.0.0.1 application.it
#   127.0.0.1 application.pl
# in your /etc/hosts file to try this out locally
def extract_locale_from_tld
  parsed_locale = request.host.split('.').last
  I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale  : nil
end

サブドメインに設定する場合は

1
2
3
4
5
6
7
8
# Get locale code from request subdomain (like http://it.application.local:3000)
# You have to put something like:
#   127.0.0.1 gr.application.local
# in your /etc/hosts file to try this out locally
def extract_locale_from_subdomain
  parsed_locale = request.subdomains.first
  I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
end

available_localesには定義済みのロケールリストが保存されている。

以下はロケールを切り替えるリンクの方法の例、APP_CONFIG[:deutsch_website_url]には「http://www.application.de」のような値が保存されている。

1
link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}")

URLパラメータからロケールを設定

URLにロケールを含めるのは最も一般的な方法である。 先の例のparamsからロケールを設定する場合、全てのリクエストに link_to( books_url(:locale => I18n.locale))) のようなロケールを含めるのは大変であるので、 ApplicationController#default_url_optionsメソッドをオーバーライドする。

1
2
3
4
5
# app/controllers/application_controller.rb
def default_url_options(options={})
  logger.debug "default_url_options is passed options: #{options.inspect}\n"
  { :locale => I18n.locale }
end

これはurl_forメソッドのデフォルト設定をオーバーライドしているので、url_forメソッドを使ったもの(root_path/routesファイルのリソースパス)全てに自動でクエリがつくようになる。

www.example.com/en/booksのようなURLでロケールを設定する場合は、routesファイルで以下のようにpath_prefix付ける。

1
2
3
4
# config/routes.rb
scope "/:locale" do
  resources :books
end

クライアント情報からロケールを設定

  • Accept-Languageを使う方法

ブラウザなどに含まれているHTTPヘッダーを使う

1
2
3
4
5
6
7
8
9
def set_locale
  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
  I18n.locale = extract_locale_from_accept_language_header
  logger.debug "* Locale set to '#{I18n.locale}'"
end
private
def extract_locale_from_accept_language_header
  request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
  • GeoIPなどのデータベースを使う方法

クライアントのIPアドレスから地域の情報をマッピングしてロケールを決める。 GeoIP Lite Countryデータベースを使うと、IPアドレスから国/地域/市などを返してくれる。

  • ユーザプロファイルからとる方法

ユーザがアプリケーション内でロケールを設定できるようにして、データベースに保存する。

アプリケーションの国際化

以上で、Ruby on RailsアプリケーションのI18nサポートの初期化が終わった。 次にL10n化を行う。 つまり、全てのロケール固有のパーツを抽象化することと、その翻訳を用意することである。

翻訳を追加

Railsの「t」ヘルパーをキーと一緒に使って以下のように書ける。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = t(:hello_flash)
  end
end
# app/views/home/index.html.erb
<h1><%=t :hello_world %></h1>
<p><%= flash[:notice] %></p>
# config/locales/en.yml
en:
  hello_world: Hello world!
  hello_flash: Hello flash!
# config/locales/pirate.yml
pirate:
  hello_world: Ahoy World
  hello_flash: Ahoy Flash

キーに該当する辞書ファイルが見つからない場合は のようなタグがHTMLに入る。 辞書ファイルを追加する場合は、サーバの再起動が必要である。 辞書ファイルにはYAMLまたはRubyファイルが使われるが、Rails開発者の中ではYAMLが推奨されている。 しかし、YAMLはスペースや特殊文字に弱く、アプリケーションがロードに失敗することもある。 Rubyファイルは最初のリクエストでアプリケーションがクラッシュするので、間違いがわかりやすい。

変数を渡して翻訳

以下のようにしてViewから辞書ファイルに変数を渡せる。

1
2
3
4
5
# app/views/home/index.html.erb
<%=t 'greet_username', :user => "Bill", :message => "Goodbye" %>
# config/locales/en.yml
en:
  greet_username: "%{message}, %{user}!"

日時フォーマットの追加

時間フォーマットをl10n化するときはRailsメソッドの「l」ヘルパーを使って、Timeオブジェクトを渡す。 ヘルパーには :format オプションも渡せる。無指定の場合は:defaultフォーマットが使用される。

1
2
3
4
5
6
7
# app/views/home/index.html.erb
<p><%= l Time.now, :format => :short %></p>
# config/locales/pirate.yml
pirate:
  time:
    formats:
      short: "arrrround %H'ish" # %Hは24時間制の時

辞書ファイルは以下で提供されているので、Gemで使うなり、config/localesにファイルを手動で置くなりして利用すると良い。 rails-i18n repository at Github(https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale)

L10n化したView

Rails2.3からロケールによるテンプレート自動選択してくれるになった。 app/views/books/index.html.erbテンプレートを用意し、同階層にindex.es.html.erbファイルを置くと、 esロケールではesのerbファイルを参照し、デフォルトはindex.html.erbファイルが参照される。

組織化された辞書ファイル

1つのロケールに対して、1つの辞書ファイルだと、増えたときに管理するのが難しくなる。 よって、以下のように階層化するなどして管理するとよい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|-defaults
|---es.rb
|---en.rb
|-models
|---book
|-----es.rb
|-----en.rb
|-views
|---defaults
|-----es.rb
|-----en.rb
|---books
|-----es.rb
|-----en.rb
|---users
|-----es.rb
|-----en.rb
|---navigation
|-----es.rb
|-----en.rb

これはモデルとビュー内のモデル属性とデフォルトで分けた例である。 このようにネストした場合は、辞書ファイルがデフォルト設定ではロードされなくなるので、以下のように 書かなければならない。

1
2
# config/application.rb
  config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]

参考

Refinements

| Comments

ruby 2.0.0の主な機能のうちのひとつ「Refinements」の挙動について。

Refinementsとは

既存のクラスの挙動を変更したり、拡張するときの影響範囲を名前空間で限定する機能です。 これまでも既存のクラスやメソッドに対する、動的な挙動の変更や追加は可能でしたが、以下のような問題もありました。

  • プログラム全体に影響を与えてしまう
  • 変更が見えにくい

モンキーパッチをモジュールとして分割してincludeするなどして、 明示的にするなどの対処法はとられてきましたが、 refinmentsは書き換えをより厳密で局所的なものにとどめるものになっています。

例としてStringクラスに局所的にメソッドを追加するコードを示します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module HogeExtension
  refine String do
    def to_camelcase
      split('_').map{|s| s.capitalize}.join('')
    end
  end
end

class RefineTest
  using HogeExtension

  def initialize
    p 'aaa_bbb_ccc'.to_camelcase
  end
end

RefineTest.new               # => "AaaBbbCcc"
p 'aaa_bbb_ccc'.to_camelcase # => NoMethodError

HogeExtensionモジュールでは、Stringクラスに対するto_calmelcaseメソッドの追加をrefineキーワードで行なっています。 この実装は、HogeExtensionモジュールをusingキーワードでとりこんだRefineTestクラスでは適用されるので、利用できます。 しかし、それ以外の部分でStringクラスのインスタンスに対しては、利用できないことがわかります。

スコープ

refinementsは厳密なレキシカルスコープ(内部で定義した変数は外から見えないが、その逆は可能というスコープ)で動きます。 つまり、usingしたスコープと違うスコープに対しては、適用されません。 また、外側よりは内部の、前のよりは後の方が優先度が高く適用されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module HogeExtension
  refine String do
    def plus_string
      p self + ' hoge'
    end
  end
end

module FugaExtension
  refine String do
    def plus_string
      p self + ' fuga'
    end
  end
end

class RefineTest
  using HogeExtension
  'outer1'.plus_string   # => "outer1 hoge"

  class Inner
    using FugaExtension
    'inner1'.plus_string # => "inner1 fuga"
  end

  'outer2'.plus_string   # => "outer2 hoge"

  using FugaExtension
  'outer3'.plus_string   # => "outer3 fuga"
end

# 拡張クラス内でも有効
class RefineTestEx < RefineTest
  'test'.plus_string     # => "test fuga"
end

モジュールには適用できない

モジュールのメソッドにはrefinementsを適用できません。

1
2
3
4
5
module EnumerableExt
  # エラー!
  refine Enumerable do
  end
end

SelectorNamespcaceである

これはちょっとややこしいので、例を示します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Hoge
  def say_hoge
    puts 'hoge'
  end

  def introduce
    print 'This is '
    say_hoge
  end
end

instance = Hoge.new
instance.say_hoge  # => 'hoge'
instance.introduce # => 'This is hoge'

module HogeExtension
  refine Hoge do
    def say_hoge
      puts 'fuga'
    end
  end
end

using HogeExtension
instance.say_hoge  # => 'fuga'
instance.introduce # => 'This is hoge'

26行目のintroduceに注目。 intstanceのintroduceが呼ぶのは元のAクラスのsay_hogeメソッドですので、’fuga’ではなく’hoge’の方が画面に出力されます。 これは、refinementsがレキシカルスコープで動くことの証拠です。 Aのintroduceメソッドは、それとは違うスコープで適用されたrefinementsの影響を受けません。 このような挙動をSelectorNamespcace と呼ぶそうです。

%i: シンボルの配列のリテラル

| Comments

Ruby 2.0.0の主な機能のうちのひとつ「%i」の挙動について。

シンボルの配列がリテラルで書けるようになった。

1
%i[hoge fuga] # => [:hoge, :fuga]

ちなみに文字列の配列のリテラルは以下のようにする。(これは以前からあった)

1
%w[hoge fuga] # => ["hoge", "fuga"]

Module#prepend

| Comments

Ruby 2.0.0の主な機能のうちのひとつ「Module#prepend」の挙動について。

prependは呼び出し元のクラス/モジュールの前にモジュールを置きます。 その呼ばれたモジュールの中で同じ名前を持つメソッドがあれば、それをラップします。 局所的なモンキーパッチを当てるような感じです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module Foo
  def foo
    p "before"
    super
    p "after"
  end
end

class Bar
  def foo
    p "bar"
  end
  prepend Foo
end

Bar.new.foo # "before", "bar", "after"

Ruby 2.0 キーワード引数

| Comments

Ruby 2.0.0の主な機能のうちのひとつ「キーワード引数」の挙動について。

キーワード引数はメソッドのそれぞれの引数にその意味を示すキーワードを付与し、キーワードを通じて引数を渡せる機能です。

1
2
3
4
5
6
7
8
9
def my_method(str: "hoge", num: 1)
  [str, num]
end

# 実行結果
my_method # => ["hoge", 1]
my_method(str: "fuga") # => ["fuga", 1]
my_method(num: 2) # => ["hoge", 2]
my_method(strrr: "huga") # => ArgumentError: unknown keyword: strrr

実行時キーワード省略でデフォルト値が利用され、キーワード指定すると指定したものだけ値が書き換えられる。 引数全てを渡さなくてもよいというのも嬉しい。 定義されていないキーワードを指定すると当然ながらエラーとなる。

Assets Pipeline

| Comments

この記事は

基本はRuby on Rails Guides: Asset Pipelineの訳ですが簡略化や自分の解釈で意訳した部分が多々あります。気になる点あったらコメントください。

Assets Pipelineとは

Asset(アセット)とは、訳すと「資産」のこと。Assets Pipelineは画像やJavaScript、CSSを高速でリクエストを捌けるようにしたRails 3.1より標準搭載された仕組みです。

Assets Pipelineで出来ること

  • Fingerprinting
    • コンテンツベースのファイル名に更新することによるキャッシュ支援
  • Precompileを利用した高レベル言語でのコーディング
    • CSSに対してSass/SCSS/LESS、JavaScriptに対してCoffeeScript等の中間言語が使用可能
    • ERBも使用可能
  • Assetの連結
    • 複数のJavascriptやCSSをまとめてリクエスト数を減らす
  • Assetの最小化と圧縮
    • CSSは空白削除、JavaScriptはif-elseを三項演算子化など、サイズ縮小のために最適化

Assets Pipelineはデフォルトで有効です。無効にしたければconfig/application.rbにて

1
config.assets.enabled = false

を設定してください。また、アプリケーション作成時であれば

1
rails new appname --skip-sprockets

と–skip-sporocketsオプションを付ける事でも無効にできます。ただし、何らかの理由が無ければ、デフォルトの状態で有効にしておくと高速化の面でも有利ですのでそのままにしておくべきでしょう。

Assets Pipelineの使い方

app/assetsディレクトリの中にAssetを配置します。今まではpublicに全て配置していて、利用は可能ですが、全て静的ファイルとしてのみ扱われます。

production環境のデフォルトでは事前にプリコンパイルを行い、public/assetsに配置して動作時にapp/assetsから直接出力せずパフォーマンスに配慮したデプロイを行います。

Assetの配置場所は

  • app/assets
  • lib/assets
  • vendor/assets

の三箇所があります。appはアプリケーション固有のもの、libは複数のアプリケーションでも利用できるもの、vendorはサードベンダーのJSプラグインやCSSフレームワークといったように使い分けると良いでしょう。

Asset Pathの検索

マニフェストやヘルパーよりファイルを参照すると、デフォルトではimages、javascripts、stylesheetsのサブディレクトリ内からファイルを探します。例えば、

1
2
3
app/assets/javascripts/home.js
lib/assets/javascripts/moovinator.js
vendor/assets/javascripts/slider.js

をJSマニフェストから参照するとすると

1
2
3
//= require home
//= require moovinator
//= require slider

ヘルパからだと

1
2
3
<%= javascript_include_tag "home" %>
<%= javascript_include_tag "moovinator" %>
<%= javascript_include_tag "slider" %>

にて参照できます。

indexファイルはフォルダ名指定でlib/assets/library_name/index.jsを

1
//= require library_name

と参照可能です。ちなみにRails.application.config.assets.pathsを見るとAsset検索Pathが分かります。また、以下のようにconfig/application.rbにパスを追加できます

1
config.assets.paths << Rails.root.join("app", "assets", "flash")

重要なことは、検索したいファイルはプリコンパイル対象となっていることです。そうしないとproduction環境では利用できなくなってしまいます。

Fingerprintingとは

Fingerprintingとはファイル内容も加味されたファイルネーミングの仕組みです。ファイルの中身が変更されるとファイル名も更新されます。

これにより、CDNであったり、複数の別のサーバー上に配置したとしても、ファイル名によってコンテンツ内容が一意である事が分かり、キャッシュ最適化を行う事が可能になります。

Assets Pipelineではファイル名にMD5ハッシュが挿入されます。global.cssが元ファイルとすると、global-908e25f4bf641868d8683022a5b62f54.cssのようなファイル名となります。

Assets Pipeline導入前のRailsは時間ベースの文字列クエリを付加していましたが、以下のデメリットがありました:

  • クエリだと確実にキャッシュする事を保証出来ない
    • CDN等でリクエストがキャッシュされないケースがあった
  • 複数のサーバに静的コンテンツを配置するとファイル名が異なる
    • ファイル時間が同一ファイルでも異なる事があるために無駄に読み込みがかかるケースがあった
  • 同一のファイルでも再度デプロイしたらキャッシュが利用されないことがある
    • 更新日時が変更されたら再度読み込みを強いることになる

これらのデメリットをファイル内容に基づくファイル名の付加によって解消されました。

Fingerprintingはproduction環境で有効、その他の環境は無効にデフォルトはなっています。替えたい場合はconfig.assets.digest設定を変更して下さい。

Assetへのリンク

Assets Pipelineでは通常のリンクヘルパを利用すればOKです

1
2
3
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
<%= image_tag "rails.png" %>

accetsパスへの検索、production環境であればMD5をファイル名に付加更新もここで行われます

1
rails-af27b6a414e6da00003503148be9b409.png

CSS/JavaScript/ERB

Assets PipelineはERBにて評価可能です。application.css.erbのようにerbの拡張子を付加すれば次のように利用できます。

1
.class { background-image: url(<%= asset_path 'image.png' %>) }

Base64形式として埋め込みヘルパもあります

1
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }

application.js.cooffee.erbのようにCoffeeScriptにも利用できます

1
$('#logo').attr src: "<%= asset_path('logo.png') %>"

アセットの依存関係と連結~マニフェストファイルとディレクティブ

どのアセットを利用するかを決定するのにマニフェストファイルを利用します。CSS/JavaScriptにコメントにてディレクティブを記述します。また、Rails.application.config.assets.compressがtrueであれば連結します。連結することに単一ファイルになるのでリクエスト時間を減らせます。例えばapp/assets/javascripts/application.jsに以下のように記述します

1
2
3
4
// ...
//= require jquery
//= require jquery_ujs
//= require_tree .

//=で行を始めてディレクティブを記載します。jquery.jsとjquery_ujs.jsはjquery-rails gemが提供しています。

require_treeは再帰検索を行います。対象パスのサブディレクトリも検索してすべてのファイルを出力します。サブディレクトリを検索しないで特定のディレクトリを指定したい場合はrequire_directoryを利用して下さい。

requireは上から順に処理されますが、require_treeは何処に記載しても順序は関係無いです。連結した場合でも、特定のJavascriptが一番最初に来るように保証したいのであれば、requireも一番最初に記述してください

CSSの例は以下のようになります

1
2
3
4
/* ...
*= require_self
*= require_tree .
*/

requre_selfはこれが呼び出された順序で自分のCSSを読み込むようになります。require_treeを使うとJSと同様に現在のディレクトリのすべてのスタイルシートを読み込みます。

複数のSassファイルを利用する時はSassの@importルールを使用スべきです。ディレクティブで記載してしまうと全Sassファイルはそれぞれのスコープ下に存在することになって変数やmixinが定義されたドキュメント内でしか利用できなくなります。

Preprocessing

Assetの拡張子で前処理が決まります。projects.js.coffeeとすればCoffeeScriptが、projects.css.scssとすればSCSSが処理されます。

複数の拡張子を付加すれば前処理の多重化も可能です。projects.css.scss.erbとすれば、拡張子は右側からERB、SCSSと処理され、最終的にCSSとして応答します。

この拡張子の処理順序は重要で、projects.js.erb.coffeeと記載してしまうと、ERB記述のままCoffeeScriptインタプリタが処理されてしまいエラーとなってしまいます。projects.js.coffee.erbの順で記載してください。

JavaScriptの圧縮

JavaScriptは:closure :uglifier :yuiの3種類が選べます。closure-compiler・uglifier・yui-compressorの各gemが必要です。Gemfileにはデフォルトでuglifierがあります。ホワイトスペースを削除し、if-else文を三項演算子に変更するなどの最適化をします。

1
config.assets.js_compressor = :uglifier

JS圧縮を有効にするには以下の設定が必要です

1
config.assets.compress = true

Development環境

development環境では連結されずに別々のファイルとなります

1
2
3
//= require core
//= require projects
//= require tickets

の記載では

1
2
3
<script src="/assets/core.js?body=1" type="text/javascript"></script>
<script src="/assets/projects.js?body=1" type="text/javascript"></script>
<script src="/assets/tickets.js?body=1" type="text/javascript"></script>

となります。但し、

1
config.assets.debug = false

と、デバッグモードをオフにすれば連結状態になります。

サーバに初回リクエストにてアセットはコンパイル・キャッシュされ、304を返すようになります。マニフェスト記載のどれかが変更があると新しくコンパイルしてファイルを応答します。

デバッグモードは以下でも有効に出来ます。

1
2
<%= stylesheet_link_tag "application", :debug => true %>
<%= javascript_include_tag "application", :debug => true %>

パフォーマンス的にはデバッグモードは不利なので切り替えが提供されています

Production環境

production環境ではFingerprintを利用します。プリコンパイル時にファイル内容に応じてMD5値が生成されてファイル名が更新されてディスクに格納されます。挿入された名前はマニフェスト、ヘルパーで補完されます。

1
2
<%= javascript_include_tag "application" %>
<%= stylesheet_link_tag "application" %>

にて

1
2
<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js" type="text/javascript"></script>
<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" type="text/css" />

となります。

config.assets.digest設定にてFingerprintのON/OFFを切り替えられます。production環境で有効、その他は無効になっています。通常はこのデフォルト設定は変更すべきではありません。キャッシュ制御がクライアント側で困難になるからです。

Assetsのプリコンパイル

development環境のデフォルトでは動的にAssetファイルがコンパイルされますが、production環境のデフォルトでは高速化を図るため、事前にコンパイルして静的ファイルとして配置するようになっています。これをプリコンパイルといいます。Railsではrakeタスクとして配布されています。

コンパイルされたAssetはconfig.assets.prefixに設定されたディレクトリに格納されます。デフォルトはpublic/assetsディレクトリです。

デプロイサーバー上でのプリコンパイルも可能ですが、ファイル書き込み権限が無い等、rakeタスクを直接実行出来ない時はローカルでrakeタスクを実行してください。実行するrakeタスクは、以下のようになります。

1
bundle exec rake assets:precompile

プリコンパイルの実行速度を早めるためにconfig.assets.initialize_on_precompile設定をfalseに設定してRailsアプリケーションのロードをプリコンパイル時には部分的に行うようにすることが可能となります。Herokuではこの設定は必須となっています。

config.assets.initialize_on_precompile設定をfalseにした際は、ローカルでプリコンパイルが正しく実行出来るか確認してからでプロイして下さい。アプリケーションのオブジェクトやメソッドを参照していたりするとエラーが発生する場合があるので注意する必要があります。

Capistano(v2.8.0以降)にはプリコンパイル実行を制御するレシピが用意されています。以下の行をCapfileに追加してください。

1
load 'deploy/assets'

これによって、config.assets.prefixはshared/assetsディレクトリに設定されます。既にこのフォルダを利用していた場合は独自にrakeタスクを作成して下さい。

また、重要な点として、古いプリコンパイルされたAssetsファイルがページキャッシュ有効な間は正しく参照可能となるようにこのsharedフォルダはデプロイサーバー間で共有されている必要があります。

Assetsファイルをローカルでプリコンパイルする時は、デプロイ環境ではAsset関連のgemが必要なくなるので、以下のコマンドでassetsグループのgemを除外すると良いでしょう。

1
bundle install --without assets

プリコンパイルを行うファイルとして、application.js、application.cssとJS/CSSファイルではない全てのファイルが含まれます。もし、他にも独自のJS/CSSファイルを読み込みたい場合は以下のように追加設定する必要があります。

1
config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']

この設定により、rakeタスクによるmanifest.ymlにはassetsファイルとそのFingerprintsの対応付けが行われ、ヘルパを呼び出すたびにSprocketsによるFingerprintへのマッピングリクエストを回避する事が出来ます。マニフェストファイルの例は以下のような感じです。

1
2
3
4
5
6
---
rails.png: rails-bd9ad5a560b5a3a7be0808c5cd76a798.png
jquery-ui.min.js: jquery-ui-7e33882a28fc84ad0e0e47e46cbf901c.min.js
jquery.min.js: jquery-8a50feed8d29566738ad005e19fe1c2d.min.js
application.js: application-3fdab497b8fb70d20cfc5495239dfc29.js
application.css: application-8af74128f904600e41a6e39241464e03.css

マニフェストファイルの配置場所はconfig.assets.prefixのルートディレクトリに配置されます。変更したい場合は以下にて設定出来ます。

1
config.assets.manifest = '/path/to/some/other/location'

Production環境でプリコンパイルされたファイルが見つからなければ、存在しないファイル名と共にSprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError の例外がRaiseされます。

サーバーの設定

プリコンパイルされたAssetファイルはファイルシステム上に静的ファイルとして配置され、Webサーバーより直接提供出来ます。Fingerprintingのメリットを十分に生かすため、以下のようにサーバー設定を行う事が推奨されます。

Apache:

1
2
3
4
5
6
7
<LocationMatch "^/assets/.*$">
  Header unset ETag
  FileETag None
  # RFC says only cache for 1 year
  ExpiresActive On
  ExpiresDefault "access plus 1 year"
</LocationMatch>

nginx:

1
2
3
4
5
6
7
location ~ ^/assets/ {
  expires 1y;
  add_header Cache-Control public;
 
  add_header ETag "";
  break;
}

プリコンパイル時、gzip圧縮(.gz)ファイルも同時に作成されます。WebサーバーではしばしばGzip圧縮転送の設定を行って転送量の軽減を図りますが、動的な圧縮だと、CPUの使用も懸念して圧縮率に妥協をすることがあります。この問題を解決するために事前に最小サイズとなるようにgzip圧縮ファイルを高圧縮率で作成した物を直接ディスクより提供する事でサーバー負荷を低減させるように設定する事もできます。

Nginxではgzip_staticを有効にする事により可能になります。設定は以下の通り。

1
2
3
4
5
6
location ~ ^/(assets)/  {
  root /path/to/public;
  gzip_static on; # to serve pre-gzipped version
  expires max;
  add_header Cache-Control public;
}

このディレクティブは大凡のコンパイル済みWebサーバーで利用出来ますが、出来ない場合はモジュールを追加してコンパイルする必要がある場合があります

1
./configure --with-http_gzip_static_module

Phusion Passengerと一緒にnginxをコンパイルする場合はこのオプションを指定する必要があります。

Apacheでも可能ですがやや困難です。トライしたいならググってください。

動的コンパイル(Live Compilation)

動的コンパイルをやりたい環境がある場合はSprocketsに全てのリクエストを投げる事も出来ます。以下のように設定します

1
config.assets.compile = true

Assetの初回アクセス時にコンパイルされdevelopment環境よりも上位環境ではキャッシュされてMD5ハッシュ化されたファイル名が使用されます。

SprocketsはCache-Controlヘッダにmax-age=31536000を設定してきます。これはサーバー、クライアント間でリクエストされたデータは1年間キャッシュしても良いという取り決めになります。この事によりブラウザ等でのローカルキャッシュや中間キャッシュを行うことができるようになり、サーバーへのリクエスト削減が期待出来ます。

しかしながら、動的コンパイルは多くのメモリやサーバ負荷となり得るのでお勧め出来ません。

もし、JavaScriptランタイムが導入されていないproduction環境のサーバーデプロイしたい場合は以下をGemfileに追加してください

1
2
3
group :production do
  gem 'therubyracer'
end

パイプラインのカスタマイズ

CSS圧縮

現在CSS圧縮エンジンとしてYUIがあります。YUIはCSSの最小化を行います。

1
config.assets.css_compressor = :yui

config.assets.compressはtrueにしておく必要があります。

JavaScript圧縮

JS圧縮は:closure、:uglifierand、:yuiの3種類から選択出来ます。それぞれ、closure-compiler、uglifier、yui-compressorのgemが必要になります。

デフォルトでGemfileにはuglifierが指定されています。これはNodeJSで書かれたUglifierJSのRubyラッパーです。スペース削減やif文を3項演算子に変換する等JSの論理変換を行ってサイズ最小化が行われます。以下のように指定してください

1
config.assets.js_compressor = :uglifier

JS圧縮指定時もconfig.assets.compressは有効である必要があります。

uglifierにはJSランタイムであるExecJSが必要になります。MacOSXやWindowsにはインストールされています。サポートされているOSに関してはExecJSのドキュメントをチェックして下さい。

独自圧縮ロジックの定義

自分で圧縮ロジックを定義する事もできます。compressメソッドで変換結果をリターンして下さい。

1
2
3
4
5
class Transformer
  def compress(string)
    do_something_returning_a_string(string)
  end
end

利用するにはオブジェクトを指定します

1
config.assets.css_compressor = Transformer.new

assetsパスの変更

Asset公開パスのデフォルトは /assets です。3.1にアップデートする際に既にこのURIが利用されていた等の際には以下にてパスを変更する事も出来ます

1
config.assets.prefix = "/some_other_path"

X-Sendfileヘッダ

X-SendfileヘッダはWebサーバー自身がアプリケーションからのレスポンスをカスタマイズしてファイルをディスク上から直接読み込んで代わりにデータの受け渡しをします。デフォルトはオフですが、サポートされているなら有効にすることによりファイル提供の転送速度向上が期待できます。

Apache、nginxでサポートされており、以下をconfig/environments/production.rbに設定します

1
2
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx

既に存在しているアプリケーションのアップグレード時等に、production環境以外のX-Sendfileに対応していない環境化にこの設定をコピペしないように注意してください。

キャッシュ

Sprocketsはdeveloment、production環境でRailsのデフォルトキャッシュストアを利用します。

デフォルトstore以外を指定出来る事が今後の課題です。

Gemsを利用したAssetsの追加

AssetsはGemsの形式でも外部ソースを取り込めます。

jquery-rails gem等が良い例です。一般的なJSライブラリを取り込めます。このgemにはRails::Engineが継承されたクラスを含んでいて、中に含まれているapp/assetsやlib/assets、vendor/assets等のディレクトをSprocketsの検索パスとして追加されます。

以上。あと、3.1以前からのアップデートの方法等載っていました。興味のある人は原文を参照してみてください。

Rubyのrequire/load/autoload/include/extendについて

| Comments

require/load/autoload/include/extend の違いについて

まず、ファイルをロードする require/load/autoload と ロードしない include/extend に分けられる。 違いについては、次で個別に説明した後に表にしてまとめる。

require

  • Kernelモジュールのメソッド
  • 同じファイルは複数回ロードされない
  • Rubyライブラリをロードする
    • RubyライブラリはRubyスクリプト(*.rb)と拡張ライブラリ(*.so,*.o,*.dllなど)を指す
  • ロードするファイルパスは、絶対パスでも相対パスでも可
  • ロードするファイル名の拡張子は自動補完してくれるため、省略可(*.rb 優先)
1
require 'ファイル名'

load

  • Kernelモジュールのメソッド
  • 同じファイルを再ロードできる
  • ロードするファイルパスは、絶対パスでも相対パスでも可
  • ロードするファイル名の拡張子は省略できない
  • 拡張ライブラリは読み込み対象ではない(できないかは不明)
  • .txtのようなファイルでもロードできる
1
load 'ファイル名'

autoload

  • Kernelモジュール(トップレベル)、Moduleクラス の2種類のメソッドがある
  • autoloadと書いた段階では、ファイルはロードされない
    • 定数const_nameを参照したときに、はじめて第2引数であるファイルが require される
    • 定数const_nameには”::”演算子が使えないため、ネストする場合は2種類のメソッドを組み合わせて使用する
  • いずれ廃止されそうなメソッド
1
autoload(const_name, 'ファイル名')

include

  • Moduleクラスのメソッド
    • Moduleクラスはクラスとモジュールを表し、ClassクラスはModuleのサブクラスになる
  • クラスやモジュールに他のモジュールをインクルード(Mix-in)する
    • 引数に指定できるのはモジュールのみ
    • 引数に複数のモジュール指定可
  • includeしたクラスのインスタンスメソッドとして使用可能
1
include(module, ...)

extend

  • Objectクラスのメソッド
  • オブジェクトの特異クラスに引数に指定したモジュールを取り込み、モジュールのメソッドを特異メソッドとして使えるようにする
    • 特異メソッドは特定のインスタンス固有のメソッドのこと
      • クラスを作った後にメソッドを追加すると特異メソッドになる
    • 特異クラスは特定のインスタンスのために用意されるクラスのこと
      • 特異クラスは特異メソッドをまとめて定義するときなどに使用される
  • extendしたクラスのクラスメソッドとして使用可能(インスタンスメソッドではない)
1
extend(module, ...)

extendは使用したことがなかったので、具体例を以下に示す。

extendの具体例
1
2
3
4
5
6
7
8
9
module Test
  def test
    puts self + "Test"
  end
end
str = "Hoge"
str.extend(Test)
str.test
>> HogeTest

require/load/autoload の違い

  require load autoload
メソッドの定義元 Kernelモジュール Kernelモジュール Kernelモジュール or Moduleクラス
複数回ロード されない される されない(requireに従うので以下省略)
ロード時の拡張子省略 不可
ロード可能ライブラリ Rubyスクリプト+拡張ライブラリ Rubyスクリプト

requireはライブラリのロードをしたいとき、loadは設定ファイルなど再読み込み可能にしたいとき、というように使い分けられる。 また、拡張子が.rbや.soなどではない.txt(中身はruby)ファイルをロードしたいときにも load を使用する。

include/extend の違い

  include extend
メソッドの定義元 Moduleクラス Objectクラス
振る舞い includeしたクラスのインスタンスメソッドとして使用 extendしたクラスのクラスメソッドとして使用

振る舞いの違いがわかりにくいので、以下に例を示す。

include/extendの例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module Hoge
  def test
    puts "HogeTest"
  end
end

class IncludeTest
  include Hoge
end
class ExtendTest
  extend Hoge
end

IncludeTest.new.test #インスタンスメソッド
>> HogeTest
ExtendTest.test #クラスメソッド
>> HogeTest

参考

Rails3 のroutes.rb

| Comments

Rails3になってルーティングの書き方がずいぶん変ったので使い方をメモ。

基本

Rails3 でのアプリケーション作り始めにて、

1
rails g scaffold article title:string

などと打つと、色々なファイルが生成され、その中にはroutes.rbも含まれている。routes.rbの覗いてみると、

1
resources :articles

という記載がある。 これはリソースCRUD操作を行うためのURLとアクションを自動で設定してくれる。 RailsでCRUDを行うために用意されている7つのアクション(index, new, create, show, edit, update, destroy)とURLとの紐付けをresourcesを使うことによって一度にすることができる。 なお、routes.rbにresources, resourceなどと書いていくわけだが、ルーティングの優先順位は上から順となっている。

現在設定されているルーティングを確認するには、rake routesコマンド用いる。

1
2
3
4
5
6
7
8
$rake routes
GET             /articles                 index        articles_path 
GET             /articles/new             new          new_article_path
POST            /articles                 create       articles_path
GET             /articles/:id             show         article_path(:id)
GET             /articles/:id/edit        edit         edit_article_path(:id)
PUT             /articles/:id             update       article_path(:id)
DELETE          /articles/:id             destroy      article_path(:id)

id不要の場合

基本で記載したresourcesを用いた場合は、

1
GET             /articles/:id             show         article_path(:id)

のように、URLにidが付加されている。 しかし、ユーザのプロフィール設定画面などidが不要な場合もあるので、そのような場合はresourcesではなく、resourceを用いる。

1
resource :profile

7つのCRUDアクションの内、indexは生成されなくなる。

1
2
3
4
5
6
7
$rake routes
GET        /profile/new      new       new_profile_path
POST       /profile          create    profile_path
GET        /profile          show      profile_path
GET        /profile/edit     edit      edit_profile_path
PUT        /profile          update    profile_path
DELETE     /profile          destroy   profile_path

ルーティングの制限

デフォルトのアクション7つのうち、不要なURLを生成したくない場合、:only 又は :except オプションを使用する。

:onlyオプションを用いると、指定したアクションのみ生成される。

1
resources :articles, :only => [:index, :new, :create, :show]

:exceptオプションを用いると、指定したアクションは生成されない。

1
resources :articles, :except => [:destroy]

ルーティングの追加

ルーティングの追加方法はリソースのidがURLに付くかどうかで、2つの方法がある。リソースidが付く方(/articles/1/preview みたいなの)をメンバールーティング、付かない方(/articles/preview みたいなの)をコレクションルーティングと呼ぶ。

メンバールーティングの追加

1
2
3
4
5
6
7
8
9
resources :articles do
  member do
    get 'preview'
  end
end
# 又は
resources :articles do
  get 'preview', :on => :member
end

コレクションルーティングの追加

1
2
3
4
5
6
7
8
9
resources :articles do
  collection do
    get 'preview'
  end
end
# 又は
resources :articles do
  get 'preview', :on => :collection
end

URLをピンポイントで指定

ログアウトのリンクなど、どのユーザでも変らないURLをピンポイントで指定するには次のようにmatchを用いる。

1
match "/authentication/logout" => "authentication#logout"

メソッドの指定

1
2
3
get "/authentication/logout" => "authentication#logout"
# 又は :viaオプションを用いて
match "/authentication/logout" => "authentication#logout", :via => :get

Named helperの設定

Named helperとはprofile_pathなどと書いているもののこと。 これを設定するには、:as オプションを使って、ルーティングのための名前を指定する。

1
match "/authentication/logout" => "authentication#logout", :as => :logout

これでlogout_pathやlogout_urlが使えるようになる。

ネストしたURL

リソースが1対他の関係にある場合、URLをネストさせることができる。 例えば、1つの記事(article)にいくつかのコメント(comment)がある場合には次のように書くことができる。

1
2
3
resources :articles do
  resources :comments
end

便利だが、ネストしすぎると混乱を招きそうなので、1回くらいにしておくとよい。 2回、3回とネストしなければならない場合、設計を見直す機会かもしれない。

1
2
3
$rake routes
GET             /articles/:article_id/comments                 index        article_comments_path
GET             /articles/:article_id/comments/:id             show         article_comment_path(:id)

トップページ(root)

デフォルトで用意されているpublic/index.htmlを消去してから、次のように記載する。

1
root :to => "articles#index"

Non-Resourceful Routes

Non-Resourceful Routesとは、任意のURLをアクションにルーティングするためのサポート。

1
match ':controller(/:action(/:id))'

というように書く。これで、GET articles/edit/1 すると、パラメータは次のようになる。

1
{:controller => "articles", :action => "edit", :id => "1"}

URLに特定の文字列を含む場合は、

1
match ':controller/:action/:id/with_user/:user_id'

として、GET /articles/show/1/with_user/2すると

1
{:controller => "articles", :action => "show", :id => "1", :user_id => "2"}

となる。

参考サイト:ruby/rails/RailsGuidesをゆっくり和訳してみたよ/Rails Routing from the Outside In - 株式会社ウサギィwiki

Enumerable::lazyをちょっとだけ

| Comments

Ruby 2.0 で追加されたEnumerable::lazyをOKIソフトウェアさんのRuby 2.0 メモ: Lazy と LINQ とループ融合を参考に動かしてみました。

まずはrvmを最新にし、ruby 2.0をインストールします。

1
2
3
rvm get latest
rvm list known # ruby 2.0 が追加されている事確認
rvm install 2.0.0-preview2

で、実験です。1から30までの整数で3が含まれるもの(3, 13, 23, 30)の最初の3つを求めるには

1
2
 (1..30).map{|i| print i; i.to_s}.select{|s| print '-'; /3/ === s}.take(3)
123456789101112131415161718192021222324252627282930------------------------------=> ["3", "13", "23"]

このコードでは、

  1. map で各整数を文字列化

  2. selectで文字列に3が出現するものを正規表現で選択する。=> [“3”, “13”, “23”, “30”]

  3. takeで先頭の三つを取り出す。=> [“3”, “13”, “23”]

で、指定した範囲の最後まで読み込んでから次の処理を行う。

これに対し、lazyを用いたコードは以下。

1
2
(1..30).lazy.map{|i| print i; i.to_s}.select{|s| print '-'; /3/ === s}.take(3).to_a
1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20-21-22-23-=> ["3", "13", "23"]

先頭の3つを読み込んだところで終了しています。 これは、結果をArrayではなくEnumerable型として返すようにmapメソッドやselectメソッドを再定義し、実際に要素の値が必要になったとき、つまり、mapの処理はselectが呼ばれたときに開始、 selectの処理はtakeに呼ばれたときに開始…というように必要なときになったら呼び出すようになっています。