Ruby Fetch

Juanito FatasThinking about what to do.

Ruby 有一個好用的 fetch 方法。可以用來取 HashArray 的值。

假設今天有一個 Oauth Hash 長這樣:

oauth = {
  'uid'  => 123456789,
  'data' => {
    'email'      => '[email protected]',
    'first_name' => nil,
    'last_name'  => 'Bar'
  },
  'credentials' => {
    'token' => "Token-3345678"
  }
}

利用上面的資料給使用者設定 Email、名字、姓氏等,可以這麼寫:

User = Struct.new(:email, :name, :last_name, :token)
juan = User.new

juan.email     = oauth['data']['email']
juan.name      = oauth['data']['first_name']
juan.last_name = oauth['data']['last_name']
juan.token     = oauth['credentials']['token']

歡迎一下使用者:

welcome = "Welcome! #{juan.name.capitalize} #{juan.last_name.capitalize}"

這時因為 Oauth Hashfirst_namenil,就會看到這個惱人的錯誤(這也是 Ruby 最常見的錯誤):

> welcome
=> NoMethodError: undefined method `capitalize' for nil:NilClass

這個錯誤訊息非常差,尤其是在大的 codebase,很難找到錯誤發生的地方,到處都有可能發生。

這時候就可以使用 Hashfetch

官方文件

fetch 在找不到鍵時,便會拋一個錯誤:

> oauth.fetch('does-not-exist')
=> KeyError: key not found: "does-not-exist"
from (pry):1:in `fetch'

會明確的告訴我們這是一個 KeyError

fetch 還可以連鎖使用:

> oauth.fetch('data').fetch('b')
=> KeyError: key not found: "b"
from (pry):2:in `fetch'

把之前的賦值改成這樣,就會在找不到資料時告訴你:

juan.email     = oauth.fetch('data').fetch('email')
juan.name      = oauth.fetch('data').fetch('name')
juan.last_name = oauth.fetch('data').fetch('last_name')
juan.token     = oauth.fetch('credentials').fetch('token')

Hash fetch官方文件,看一下 fetch 接受的參數有什麼:

fetch(key [, default] )
fetch(key) {| key | block }

可看出來有兩種形式,「一個參數+可選參數」,或是「單一參數+區塊」。

這個“可選參數”與“區塊”就是用來設定,fetch 抓不到給定 key 的值時,用來回傳的預設值。

以下繼續使用上面的 oauth Hash 來示範。

> oauth.fetch('dataaaaaa', 42)
=> 42

> oauth.fetch('dataaaaaa') { 42 }
=> 42

都可以設定預設值,這兩者有什麼區別呢?看下例:

假設今天預設值不是 42 這麼簡單,是個“昂貴的計算”,耗時三秒:

# 電腦會睡 3 秒
def expensive_computation
  sleep 3
end

key 不存在時,兩種形式都會睡 3 秒:

> oauth.fetch('dataaaaaa', expensive_computation)
=> # 會睡 3 秒

> oauth.fetch('dataaaaaa') { expensive_computation }
=> # 會睡 3 秒

但是,在 key 存在時:

> oauth.fetch('data', expensive_computation)
=> # 會睡 3 秒,再返回值。
=> {"email"=>"[email protected]", "first_name"=>"Foo", "last_name"=>"Bar"}

> oauth.fetch('data') { expensive_computation }
=> # 直接返回值
=> {"email"=>"[email protected]", "first_name"=>"Foo", "last_name"=>"Bar"}

上例可看出,不管 key 存不存在,參數形式每次都會對預設值求值;

而區塊形式只在 key 不存在時,才做求值。

記得使用 fetch 時,預設值用“區塊形式”!

文件見此

> arr = [*1..5] # 用來建構 [1,2,3,4,5] 的簡寫
=> [1, 2, 3, 4, 5]

> arr[6]
=> nil

> arr.fetch(6)
=> IndexError: index 6 outside of array bounds: -5...5
from (pry):33:in `fetch'

在找不到元素時,會明確的告訴你“索引”錯誤。

提供預設值的方法同上,參數形式與區塊形式。

fetch 區塊預設會傳入找不到的 keyHash)或是索引(Array)給區塊。

arr = [*1..5]
hash = {}

arr.fetch(6) do |index|
  puts "Given index #{index} out of range, please provide a value:"
  gets.chomp # chomp 用來消掉輸入的“換行符”
end

hash.fetch('not-exist') do |key|
  puts "Missing key #{key}, please provide a value:"
  gets.chomp # chomp 用來消掉輸入的“換行符”
end

區塊還可以這麼傳,去除重複:

get_default_value = ->(key_or_index) do
  puts "#{key_or_index} not found, please enter it: "
  gets.chomp
end

arr.fetch(6, &get_default_value)
hash.fetch('not-exist', &get_default_value)

這種區塊形式是 Ruby 1.9 的 lambda 語法

有人說,為什麼不用 || 就好了?請見下例:

> {}[:foo] || :default
=> :default
> {foo: nil}[:foo] || :default
=> :default
> {foo: false}[:foo] || :default
=> :default

# 使用 fetch

> {}.fetch(:foo) { :default }
=> :default
> {foo: nil}.fetch(:foo) { :default }
=> nil
> {foo: false}.fetch(:foo) { :default }
=> false

fetch 分的出是“沒有 key”,還是 key 的值是 nilfalse

:)