高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Ruby 語法放大鏡之「常在終端機裡下 rake db:migrate 指令,這個 rake 是什麼,後面那個 db:migrate 又是怎麼回事?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在開發 Ruby on Rails 專案的過程中,一定都看過或用過 rake db:migrate 這個指令,大部份的教學資料可能只會跟你說「只要照著打這個指令就行了」,沒有太多詳細的介紹。

其實這個 rake 指令,是一位已過世但我非常景仰的大師 Jim Weirich 所開發的。如果各位曾經聽說過 Make 這個工具,Rake 就像是 Ruby 版的 Make,你可以用 Ruby 語法來編寫 makefile。

動手練習

隨便建一個目錄來練習一下:

$ cd /tmp
$ mkdir rake_demo
$ cd rake_demo
$ rake
rake aborted!
No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval'
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `<main>'
(See full trace by running task with --trace)'

執行 rake 指令後,它會預期在這個目錄有 rakefile, Rakefile, rakefile.rb, Rakefile.rb 這四個檔案任何一個檔案,但因為什麼都沒有所以出現上面的錯誤訊息。既然沒有,就做一個給它:

$ touch Rakefile
$ rake
rake aborted!
Don't know how to build task 'default' (see --tasks)
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval'
/Users/user/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `<main>'
(See full trace by running task with --trace)'

咦? 有 Rakefile 但還是有錯誤訊息,那是因為我們什麼都還沒寫,所以先打開 Rakefile 來寫一個簡單的寄信任務:

1
2
3
4
5
6
7
desc "寄發會員通知信"
task :send_email do
  # ... 寄發信件功能實作
  puts "Email Sent!"
end

task default: [:send_email]

task 方法可以定義任務的名字,後面接的 block 就是它實作的內容(實作功能就留給大家了,這裡僅先使用 puts 方法把結果印出來)。最上面的 desc 則僅是這個任務的描述,如果有描述的話,在 rake -T 指令列出任務清單的時候就看得到說明了:

$ rake -T
rake send_email  # 寄發會員通知信

因為我在上面定義了一個叫做 send_email 的任務,如果要執行它的話,就是這樣下指令:

$ rake send_email
Email Sent!

又,因為這邊我在最後一行把預設的任務設定成 send_email,所以即使我只輸入 rake 指令,它也會執行 rake send_email 任務。

另外,如果任務沒有在 rake -T 的清單上不表示任務不存在,它可能只是 desc 沒寫而已,但一樣是可以執行的喔。

那 rake db:migrate 中間的冒號?

了解 rake 的基本操作後,回來看看 Rails 裡常用的那個 rake db:migrate 是怎麼做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
desc "寄發會員通知信"
task :send_email do
  # ... 寄發信件功能實作
  puts "Email Sent!"
end

namespace :db do
  desc "Migrate the database"
  task :migrate do
    puts "migrating database!"
  end
end

task default: [:send_email]

使用 namespace 方法,然後把任務包進去,這樣一來,任務的名字就會長得像這樣:

$ rake -T
rake db:migrate  # Migrate the database
rake send_email  # 寄發會員通知信

而且可以正常執行:

$ rake db:migrate
migrating database!

大概就是這樣囉! 關於更多 Rake 的使用方式,請參閱 Rake 的 Github 網站說明 https://github.com/ruby/rake

Rails 裡也是這樣嗎?

隨便開一個 Rails 專案,在根目錄應該可以看到一個 Rakefile,它的內容長這樣:

1
2
3
4
5
6
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative 'config/application'

Rails.application.load_tasks

咦? 怎麼都空空的? 讓我們直接挖 Rails 的原始檔出來看看(以 Rails 5.0.0 beta 4 版本為例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 檔案:railties/lib/rails/tasks.rb

require 'rake'

# Load Rails Rakefile extensions
%w(
  annotations
  dev
  framework
  initializers
  log
  middleware
  misc
  restart
  routes
  tmp
).tap { |arr|
  arr << 'statistics' if Rake.application.current_scope.empty?
}.each do |task|
  load "rails/tasks/#{task}.rake"
end

這段程式碼的意思就是一口氣載入一堆放在 railties/lib/rails/tasks/ 目錄的一些檔案(.rake 檔),這些都是 Rails 預設會載入的任務。而那個 rake db:migrate 任務內容就藏在(這裡)。

要在 Rails 專案加上自己的 rake 指令?

在剛剛看到根目錄的 Rakefile 的最後一行寫到:

1
Rails.application.load_tasks

再繼續練習挖一下原始碼,可以翻到這個檔案,這個檔案大概 700 行左右,我只列出相關的程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
# 檔案:railties/lib/rails/engine.rb

def load_tasks(app=self)
  require "rake"
  run_tasks_blocks(app)
  self
end

def run_tasks_blocks(*)
  super
  paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end

看得出來 Rails 除了載入內建的任務外,其中這個 load_tasks 方法會把在專案裡 lib/tasks 目錄裡的任務檔也都一併讀進來。

除了挖原始碼看得出來之外,其實在 Rails 專案根目錄的 Rakefile 一打開的那兩行說明:

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

如果你想要加自己的 task 的話,就是寫在專案的 lib/tasks/ 目錄裡。寫法跟上面寫 Rakefile 沒什麼兩樣,更多細節可參閱 Rails Guide 上的 Command Line 的 Custom Rake Tasks 章節。

Rails 5 的變化

提醒一下,在 Rails 5 之後,原本那些 rake 指令,例如:

$ rake db:migrate
$ rake routes

也都搬一份到 rails 指令底下囉,像是這樣:

$ rails db:migrate
$ rails routes

下次看到這樣的指令,不要以為是打錯字囉 :)

Ruby 語法放大鏡之「gem install 之後,那些 gem 安裝到哪裡去了?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在 Ruby 要安裝套件超簡單的,只要 gem install 指令加上套件的名字,敲完按下 Enter 鍵,就自動會連上網路、下載套件、安裝套件,一氣呵成:

$ gem install takami
Fetching: takami-0.0.1.gem (100%)
Successfully installed takami-0.0.1
Parsing documentation for takami-0.0.1
Installing ri documentation for takami-0.0.1
Done installing documentation for takami after 0 seconds
1 gem installed

如果該套件有需要其它套件,它也會一併順便一起下載、安裝。這個 takami gem 是我自己做的,其實沒有任何內容在裡面,所以可安心安裝!(咦?!)

所以我說那個套件呢?

安裝套件很簡單,但安裝好了之的那些檔案放哪去了? 執行 gem environmentgem env 可列出目前在這台電腦的設定:

$ gem env
RubyGems Environment:
  - RUBYGEMS VERSION: 2.5.1
  - RUBY VERSION: 2.3.1 (2016-04-26 patchlevel 112) [x86_64-darwin14]
  - INSTALLATION DIRECTORY: /Users/user/.rvm/gems/ruby-2.3.1
  - USER INSTALLATION DIRECTORY: /Users/user/.gem/ruby/2.3.0
  - RUBY EXECUTABLE: /Users/user/.rvm/rubies/ruby-2.3.1/bin/ruby
  - EXECUTABLE DIRECTORY: /Users/user/.rvm/gems/ruby-2.3.1/bin
  - SPEC CACHE DIRECTORY: /Users/user/.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /Users/user/.rvm/rubies/ruby-2.3.1/etc
  - RUBYGEMS PLATFORMS:
  ... 略

那個 INSTALLATION DIRECTORY 就是 gem 安裝的地方,裡面翻一下應該就可以找得到剛剛安裝的 takami 套件了。因為我是使用 RVM,所以 gem 的安裝路徑會在 .rvm 目錄裡。

打包帶走

如果想要下載這些套件的原始檔,可至 RubyGems 官網下載:

image

下載的回來的是一個 .gem 檔(例如:takami-0.0.1.gem),這個 .gem 本身也可以直接拿來安裝,同樣是使用 gem install 指令:

$ gem install takami-0.0.1.gem

這樣一樣可以達到安裝的目的。如果你對這個 .gem 檔的內容有興趣,可以使用 gem unpack 指令把它解開:

$ gem unpack takami-0.0.1.gem
Unpacked gem: '/private/tmp/takami-0.0.1'

其實 .gem 檔案本質上就只是個壓縮檔,很多解壓縮程式應該都打得開。或是你本來就已經有安裝那個 gem 的話,也可以直接這樣做:

$ gem unpack takami
Unpacked gem: '/private/tmp/takami-0.0.1'

gem unpack 這個指令可以把 gem 解開到目前所在的目錄了(所以如果你解開我這個 takami 的 gem 就會真的看到裡面一點料都沒有了,是個空包彈)

不過通常這些 gem 的作者都會順便提供 Github 連結,直接上去看會更方便。

Ruby 語法放大鏡之「常在 controller 裡看到 before_action,它是一個方法嗎? 跟一般用 def 定義的有何不同?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在 Rails 專案中,常會在 Controller 裡看到這樣的寫法:

1
2
3
4
5
6
7
8
9
10
class ProductController < ApplicationController
  before_action :find_product, only: [:show, :edit, :update, :destroy]

  # .. 中略

  private
  def find_product
    @product = Product.find_by(id: params[:id])
  end
end

這是指在執行特定 Action 之前,先去執行 find_product 方法。

在 Model 裡也常看到類似的寫法:

1
2
3
4
5
6
7
8
9
class Article < ApplicationRecord
  before_save :encrypt_password
  validates :title, presence: true

  private
  def encrypt_password
    # ....
  end
end

這是說在資料儲存之前,先執行 encrypt_password 方法對密碼進行加密。

在一般的 Ruby 專案裡也常會看到這樣的寫法:

1
2
3
4
5
6
7
8
9
10
class Cat
  attr_accessor :age, :name
end

kitty = Cat.new
kitty.name = "nancy"
kitty.age = 10

puts kitty.name   # => nancy
puts kitty.age    # => 10

所以,在 Controller 裡的 before_action 或是 Model 裡 before_savevalidates,或是那個 attr_accessor 到底是什麼來歷? 感覺好像是什麼設定還是屬性,或是關鍵字之類的東西?

類別方法

其實,這些看起來像設定或是屬性的東西,它們不過就是類別方法罷了。

1
2
3
4
5
6
7
class Cat
  def self.my_attr_accessor(*field_name)
    # ... 實作
  end

  my_attr_accessor :age, :name
end

跟寫一般的方法沒什麼不同,一樣是使用 def 這個關鍵字來定義,只是在定義類別方法的時候,需要在方法名字前面加個 self ,這樣就可以定義出類別方法(事實上是 Singleton Method),然後就可以直接在類別裡面使用。

至於這些類別方法裡面的實作稍微有些複雜,有的需要知道怎麼操作 Block,有的還需要知道怎麼動態的定義方法(例如使用 define_method),我們另外再開一篇來介紹。

關鍵字?

有些人以為 privateprotected 這些語法是關鍵字,事實上這些都只是方法罷了(Ruby 什麼東西沒有,方法最多了)。

Ruby 裡的關鍵字不算太多,有興趣可翻 Ruby 的原始碼出來看看,Ruby 大概定義了 40 個左右的關鍵字,大概就是 ifelsedefclass 之類的。參考連結

但即使是關鍵字也不表示全部不能用,你要拿來定義方法也是可以的,例如:

1
2
3
def if
  puts "this is a if"
end

在定義的時候不會出錯,但使用的時候就沒辦法了:

1
if()

會得到錯誤訊息如下:

1
2
-:51: syntax error, unexpected end-of-input
shell returned 1

因為 Ruby 在判讀語法的時候,期待 if 後面還有一些東西,所以 Ruby 認為你還沒寫完然後被判定成語法錯誤。但如果是包在類別裡的話:

1
2
3
4
5
6
7
class Joke
  def if
    puts "this is a if"
  end
end

Joke.new.if

這樣就能正常執行了 :)

Ruby 語法放大鏡之「如果想要找某個方法的定義,該怎麼找?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在寫程式的過程中,如果遇到某個方法不會用或不知道該傳什麼參數怎麼辦? 不要急著翻 Google 或 Stackoverflow 網站,先翻官方的使用說明手冊吧,RTFM 是最好的方法!

除了翻手冊之外,像 Ruby 或 Rails 這樣 open source 的工具,另一個好方法就是直接翻它的原始碼出來看,看看它是怎麼實作的。

原始碼挖挖挖!

在開發 Ruby / Rails 專案時,通常我遇到一些很有趣或很有點厲害的寫法,就會想挖出來看看它是怎麼做的。直接開 Rails Console 來試一下:

1
2
3
4
5
$ rails console
Loading development environment (Rails 5.0.0.beta3)

>> 3.days.ago
=> Sun, 24 Apr 2016 09:02:02 UTC +00:00

3.days.ago 這樣就能印出三天前的時間,超方便! 但它怎麼做的…?

你可以使用 method 這個方法來回傳一個 Method 類別的實體,並且準備對它做一些操作:

1
2
3
# 我先用 method 方法取得 `days` 這個方法物件
>> 3.method(:days)
=> #<Method: Fixnum(Numeric)#days>

雖然「用 method 這個 method 回傳一個 Method 的實體」這句話唸起來超饒舌,但這個方法取得的物件還滿好用的,例如:

1
2
3
# 使用 owner 方法取得定義這個方法的類別或模組
>> 3.method(:days).owner
=> Numeric

看來是定義在 Numeric 這個類別或模組裡。使用 source_location 可以挖出檔案在哪:

1
2
>> 3.method(:days).source_location
=> ["/Users/user/.rvm/gems/ruby-2.3.0/gems/activesupport-5.0.0.beta3/lib/active_support/core_ext/numeric/time.rb", 45]

既然檔案路徑都有了,就可以去打開來看看是怎麼寫的了。但如果是 Ruby 內建的方法,就沒辦法直接取得原始碼的路徑,source_location 方法會回傳 nil:

1
2
>> method(:puts).source_location
=> nil

那怎辦? 例如想要知道陣列內建的 sort 是怎麼做排序的話,就去翻翻官方手冊吧。

連到官方手冊,跳轉到關於 Array 類別的 sort 章節:

image

除了介紹以及使用範例外,如果對原始檔有興趣,也可以點一下右上角的那個「放大鏡」,看看這個方法是怎麼用 C 語言實作的:

image

然後就可以再繼續追下去了。如果對 Ruby 的原始碼有興趣,也可在 Github 上取得完整的原始碼。連結

小結

曾有朋友問,要怎麼樣才能從新手變成比較資深的開發者。

除了官網的手冊要看之外,有空挖挖原始碼、讀原始碼,看看這些大大是怎麼做出這些功能進而吸收變成自己的經驗值,都是讓你可以更快變強的好方法 :)

Ruby 語法放大鏡之「為什麼 2.days.ago 在內建的 irb 會找不到這個方法? 這不是 Ruby 語法嗎?」

Ruby 語法放大鏡」系列短文主要是針對在大家學習 Ruby 或 Rails 時看到一些神奇但不知道用途的語法介紹,希望可以藉由這一系列的短文幫大家更容易的了解到底 Ruby 或 Rails 是怎麼回事。

在公開演講或是校園推廣 Rails 的時候,我常會開 rails console 出來秀一下 Rails 快速開發的威力:

1
2
3
4
5
6
7
8
$ rails console
# 看,這樣寫就可以印出 2 天前的時間
>> 2.days.ago
=> Sun, 24 Apr 2016 00:56:51 UTC +00:00

# 這樣可以印出 10 Megabyte 是多大
>> 10.megabyte
=> 10485760

這真的很驚人,不只程式碼 2.days.ago 本身看起來好寫,而且就算連不懂程式語言的人看了也大概猜得出來是什麼意思,台下觀眾看到這裡,有的就會開始發出「喔喔喔」的讚嘆聲了。

但打開標準的 irb 來試試:

1
2
3
4
5
6
7
8
>> 2.days.ago
NoMethodError: undefined method `days' for 2:Fixnum
 from (irb):1
 from /Users/user/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `<main>'
>> 10.megabyte
NoMethodError: undefined method `megabyte' for 10:Fixnum
  from (irb):2
  from /Users/user/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `<main>'

一樣的語法在 Ruby 竟然不會動了!

原來,這些看起來很厲害的語法是 Rails 幫 Ruby 做的擴充功能,讓開發者在開發網站應用程式的時候可以很快的把功能完成。

怎麼做的?

先來看一段範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat
  def sleep
    puts "ZZZZZZ"
  end
end

class Cat
  def eat
    puts "Yammy!"
  end
end

kitty = Cat.new
kitty.eat         # => Yammy!
kitty.sleep       # => ???

這裡我不小心定義了兩次 Cat 類別,如果這是你剛接觸 Ruby 不久,你也許會認為後面定義的類別會蓋掉前面的類別,所以執行 eat 方法沒問題,但執行 sleep 就會壞掉了。

但實際執行的時候發現其實兩個方法都可以正常運作! 所以,上面這段範例到底發生了什麼事?

事實上,當 Ruby 遇到兩個類別重複定義的時候,後面的定義並不會「覆蓋」掉前面的,反而比較像是「合併」。這個設計在 Ruby 裡面稱之「Open Class」,可以「打開」已經存在的類別,並再加料進去,甚至連內建的類別(像是數字、字串、陣列之類的)也可以。

我們來試試看內建的字串類別:

1
2
3
4
5
class String
  def say_hello
    "hello, #{self}"
  end
end

在這個定義之後,所有的字串就都會 say_hello

1
2
3
puts "kitty".say_hello  # => "hello, kitty"
puts "snoopy".say_hello  # => "hello, snoopy"
puts "くまモン".say_hello  # => "hello, くまモン"

這在其它程式語言是很少見的設計,特別是連內建的類別也可以這樣玩。讓我們再來玩一個!

1
puts 1 + 2    # => 3

1 加 2,不就等於 3 啊,這小學生都會!

但你知道其實這個簡單的加法可能跟你想的不太一樣,Ruby 的四則運算,事實上並不是一般的運算元(operator)。以上面這個 1 + 2 的例子來說,在 Ruby 裡實際上是:

數字物件 1 呼叫了 `+` 這個方法,並且把數字物件 2 傳進去當做參數

所以 1 + 2 事實上是長這樣:

1
puts 1.+(2)  # => 3

執行之後會得到一樣的結果。既然知道加法其實也是一個 Ruby 的 method 的話,那就可以來試著這樣玩:

1
2
3
4
5
6
7
class Fixnum
  def +(n)
    1000
  end
end

puts 1 + 2

這樣一來,不管是 1 + 2 或是 3 + 4,得到的結果都會是 1000。

不過這樣做有點風險,因為這樣等於是改寫了加法的行為,如果有其它方法有用到這個方法的話也會跟著得到不正確的結果。上面這個例子可以再改寫成:

1
2
3
4
5
6
7
8
9
10
class Fixnum
  alias :original_plus :+

  def +(n)
    puts "hey hey hey"
    original_plus(n)
  end
end

puts 1 + 2

執行上面這段範例可以發現,1 + 2 還是等於 3,但除此之外還會默默的輸出 hey hey hey 字樣,表示除了原來的加法之外,還可以做一些事。至於可以做什麼,這就靠大家自己發揮想像力了。

所以,Rails 是怎麼做的?

Rails 使用了 Open Class 的手法來幫 Ruby 加了不少功能,就以 2.days.ago 來說:

1
2
3
4
5
6
7
8
9
10
class Numeric
  # .. 略

  def days
    ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
  end
  alias :day :days

  # .. 略
end

這裡打開了 Numeric 類別(它是 Fixnum 的上層類別),幫它加上了 days 方法,而且還很貼心的幫 days 做了單數的 alias day,萬一只有 1 天也可以寫出 1.day.ago 這樣更像英文的語法。

實作的程式碼請看這裡

安全嗎?

這種「猴子補丁 (Monkey Patching)」的手法,有些人覺得很不嚴謹 (但我自己個人覺得還好),在 Ruby 2.0 之後推出了一個叫做 Refinement 的設計,可以稍微控制一下 Open Class 影響範圍。以前面那個 "kitty".hello 的範例,重新用 Refinement 改寫會長像這樣:

1
2
3
4
5
6
7
module MyHelloString
  refine String do
    def say_hello
      "hello, #{self}"
    end
  end
end

上面這段範例建立了一個叫 MyHelloString 的模組,在裡面使用 refine 方法在 String 類別上面進行「提煉」。但這並不會像之前 Open Class 的一樣寫完馬上有效果,要使用這個提煉過的方法,則是使用 using 方法:

1
2
3
using MyHelloString

puts "kitty".say_hello  # => "hello, kitty"

小結

各位看官會覺得 Ruby 的 Open Class 很隨便嗎? 我自己是還滿愛這樣的設計的 :)

Ruby 是一款非常有彈性的程式語言,但彈性本身也是雙面刃,你得知道學會怎麼運用這個彈性,而且你最好知道自己在做什麼。