5240


coffeescriptで

[code]

    user = if Math.random() > 0.5
      {items:[{place:1000}]}
    else
      {items:[{place:1000},{place:2000}]} 
    
    place = user?.items?[1]?.place || 0
    console.log place
    
[/code]

とすると、userオブジェクトの中身がどんなに複雑でも、要素があればその値、無ければ0を取得するということが出来てとても便利だと思っているのですが、これをrubyで実現することは可能なのでしょうか。


CC BY-SA 3.0
2015-01-25T04:32:24.373
5235
2015-01-26T01:33:31.203
2015-01-25T06:19:00.603
5840
question
ruby coffeescript
rubyで存在しない要素にアクセスしたときに返すデフォルト値を指定するには？


ActiveSupport の Object#try を利用すると、同じような事が出来ます。これは、メソッドが無ければ nil を返し、そうで無ければ
send と同じ動きをするものです。

[code]

    require "active_support/all"
    user = {items:[{place: 1000}]}
    # => {:items=>[{:place=>1000}]}
    
    user[:items][1][:place]
    # => NoMethodError: undefined method `[]' for nil:NilClass
    
    user.try(:[], :items).try(:[], 1).try(:[], :place)
    # => nil
    user.try(:[], :items).try(:[], 1).try(:[], :place) || 0
    # => 0
    
    
    user = {items:[{place: 1000}, {place: 2000}]}
    user.try(:[], :items).try(:[], 1).try(:[], :place)
    # => 2000
    user.try(:[], :items).try(:[], 1).try(:[], :place) || 0
    # => 2000
    
[/code]

変数に対しては、`defined?` を利用することになります。(現実的には、変数自体が nil
であることがあっても、定義されていないというのはあまり無いと思いますが。)

[code]

    ((defined? user) ? user : nil).try(...)
    
[/code]

または

[code]

    user = nil if not defined? user
    
[/code]


CC BY-SA 3.0
2015-01-25T06:31:35.763
5240
2015-01-25T06:31:35.763
76
5235
answer


もし遊び心気分なら、`method_missing`などの組み込み関数を使って、いたずらができます。

[code]

    # メソッドがないときに、このクラスが返されます。
    class DeadClass
      def method_missing(*args)
        self
      end
    
      def だ
        nil
      end
    
      def try_index(*args)
        self
      end
    end
    
    # ブラケットがメソッド名に表示するのは不正ですから、このクラスはインデックスのときに返されます。
    class Indexer
      def initialize(obj)
        @obj = obj
      end
    
      def [](*args)
        @obj.try_index(*args)
      end
    end
    
    class Object
      def method_missing(name, *args)
        if name.to_s[0] == 'か'
          realname = name.to_s[1..-1].to_sym
    
          if self.respond_to? realname
            self.send(realname, *args)
          else
            DeadClass.new
          end
        else
          raise NoMethodError.new("NoMethodError")
        end
      end
    
      def だ
        self
      end
    
      def か
        Indexer.new(self)
      end
    
      def try_index(*args)
        if self.respond_to? :"[]"
          self[*args]
        else
          DeadClass.new
        end
      end
    end
    
[/code]

使い方は、「か」とメソッド名前につけて、「だ」と行のあとにつけます。

[code]

    # 「だ」と`nil`か値になります。
    1.かsucc.かsucc.だ
    # => 3
    
    nil.かsucc.かsucc.だ
    # => nil
    
    user = {items:[{place: 1000}]}
    user.か[:items].か[1].か[:place].だ || 0
    # => 0
    
    user.か[:items].か[0].か[:place].だ || 0
    # => 1000
    
[/code]

Coffeescriptの方がいいけど、上のようにするとRubyでも多かれ少なかれ似せることができます。少なくとも構文がActiveSupportよりいいと思います。


CC BY-SA 3.0
2015-01-25T08:50:16.457
5245
2015-01-25T17:06:09.263
2015-01-25T17:06:09.263
7745
7745
5235
answer


Hashieというgemと`method_missing`（NullObjectパターン）を組み合わせて、CoffeeScriptのシンタックスにできるだけ近づけてみました。

[code]

    require 'hashie'
    
    include Hashie::Extensions::MethodAccess
    
    module NullObject
      refine NilClass do
        def method_missing(m,*a,&b)
          self
        end
      end
    end
    using NullObject
    
    5.times do
      user = rand > 0.5 ? {items:[{place:1000}]} : {items:[{place:1000},{place:2000}]}
      place = user.items[1].place || 0
      p place
    end
    
[/code]

実行結果

[code]

    2000
    0
    2000
    0
    0
    
[/code]

CoffeeScriptでは `user?.items?[1]?.place || 0` になっていますが、こちらは
`user.items[1].place || 0` のように `?` が付かないのがCoffeeScript版と異なる点です。

参考文献

  * <https://github.com/intridea/hashie>
  * <http://melborne.github.io/2014/03/07/nullobject-pattern-in-ruby/>


CC BY-SA 3.0
2015-01-25T10:57:04.197
5248
2015-01-25T11:11:25.057
2015-01-25T11:11:25.057
85
85
5235
answer


制御構造のrescue修飾子を利用するのが一番簡単かと思います。<http://docs.ruby-
lang.org/ja/2.1.0/doc/spec=2fcontrol.html#begin>
の少し下を参照してください。存在しない要素にアクセスした時にデフォルト値を指定するのと少し違いますが、存在しない要素にアクセスした時にnilが返った後の処理で、例外が発生して
rescue の後ろの値が返されます。  
この書き方では以下で元の動作と同じになります。

[code]

    user = if rand > 0.5
             {items:[{place:1000}]}
           else
             {items:[{place:1000},{place:2000}]}
           end
    place = (user[:items][1][:place] rescue nil) || 0
    puts place
    
[/code]


CC BY-SA 3.0
2015-01-26T01:33:31.203
5282
2015-01-26T01:33:31.203
4372
5235
answer