SugarCubeのAnonymousを紹介

  • 8
    Like
  • 0
    Comment
More than 1 year has passed since last update.

katsuyoshiです。
この記事はRubyMotion Advent Calendar 2013の13日目です。
そして、今日は13日の金曜日ですね。皆さんが今日一日何事もなく過ごせる事を願っています。

13日目ではみんな大好きSugarCubeの中からAnonymousを紹介します。
余談ですが、Anonymousのスペルはなかなか覚えれなくていつも間違ってしまいます。
このクラスは私が送ったパッチが切っ掛けで追加される事になりました。

さて本題です。
簡単なデータを扱う場合はHashやArrayを用いると思います。

例えば13日の金曜日にちなんで次の様なデータがあるとします。

app_delegate.rb
@jason_info = {
  name:"Jason Voorhees",
  movies: [
    { title:"Friday the 13th", year:1980 },
    { title:"PART2 Friday the 13th Part 2", year:1981},
    { title:"PART3 Friday the 13th Part 3", year:1983},
    { title:"Friday the 13th : The Final Chapter", year:1984},
    { title:"Friday the 13th Part V : A New Beginning", year:1985},
    { title:"Friday the 13th Part VI : Jason Lives", year: 1986},
    { title:"Friday the 13th Part VII : The New Blood", year: 1988},
    { title:"Friday the 13th Part VIII : Jason Takes Manhattan", year: 1989},
    { title:"Jason Goes to Hell : The Final Friday", year: 1993},
    { title:"Jason X ", year: 2002},
    { title:"FREDDY VS. JASON", year: 2003},
    { title:"Friday the 13th", year: 2009}
  ]
}

中身を取り出すにはこんな感じになりますよね

app_delegate.rb
@jason_info[:name] # => "Jason Voorhees"
@jason_info[:movies].first[:title] # => "Friday the 13th"
@jason_info[:movies].first[:year] # => 1980

Anonymous

Anonymousを使った場合はどうなるかやってみます。
まずはテスト用のプロジェクトを作ります。

$ motion create friday_the_13th
$ cd friday_the_13th

Gemfileにsugarcubeを使う事を記述します。
requireオプションに'sugarcube-all'を指定すると全部が使えるので手っ取り早いです。

# Gemfile
gem 'sugarcube', :require => 'sugarcube-all'

しかし、SugarCubeは肥大化してきていてバイナリーサイズが大きくなってしまいました。
そこで、バージョン1.0からは必要な部分を選択して使う事ができる様になり、バイナリーのサイズを小さくする事ができるようになっています。

# Gemfile
gem 'sugarcube', :require => [
  'sugarcube-anonymous'
]

今回はこれでいきましょう。
ちなみにsugarcube-allを指定した時のシミュレータ用のバイナリーサイズは9.6MBでしたが、sugarcube-anonymousのみにした時は3.8MB(BubbleWrapも含む)になりました。
rake cleanしてから再Buildしないと小さくならないので注意して下さい。

これで準備が整いました。
後は普通にコードを書いて実行するだけです。

コードを書いて行きます。
先程の@jason_infoに対して#to_objectを呼び出すとAnonymous objectが得られます。

app_delegate.rb
jason = @jason_info.to_object

AnonymousはHashを継承したクラスなので実態はHashなのですが、まるでクラス定義してインスタンス変数が定義されてるかの様に振る舞います。

app_delegate.rb
jason.name # => "Jason Voorhees"

更に、:moviesと関連づけられた配列内までネストされ、同様にアクセスできます。

app_delegate.rb
jason.movies.first.title # => "Friday the 13th"
jason.movies.first.year # => 1980

値の変更もできます。

app_delegate.rb
jason.name = "ジェイソン・ボーヒーズ"

今度はjason objectにもう少し情報を与えてみます。

app_delegate.rb
jason.category = "Horror" # => NoMethodError

Oops! 元々入っていないキーにはアクセスできないのでした。

Hashとしてデータを与えてからなら大丈夫です。

app_delegate.rb
jason[:category] = nil
jason.category = "Horror"

キーとしてSymbolを与えてますがStringでも大丈夫です。
説明の為に今度はBubbleWrapの力も借ります。
Gemfileにbubble-wrapを追加します。

# Gemfile
gem 'bubble-wrap'

データはファイルに保存して、再度読込み再現する事は日常茶飯事ですね。
ここではJSONファイルとして保存して再現する事にしてみます。

app_delegate.rb
json = BW::JSON.generate jason
# ここでファイルに保存したつもり
# ここでファイルから読み込んだつもり
revived_jason_info = BW::JSON.parse(json) # => {"name":"ジェイソン・ボーヒーズ", ... }

jasonだったりjsonだったりでややこしいですね。
revived_jason_infoを見てみると、jsonデータから再現した時にSymbolだったキーがStringになっています。
保存する前は@jason_info[:name]で名前を取得できましたが、再現したrevived_jason_infoではrevived_jason_info["name"]としないと正しく読取れません。jasonとjsonの様にややこしいですね。

revived_jason_infoもAnonymous化してみましょう。

app_delegate.rb
revived_jason = revived_jason_info.to_object
revived_jason.name #=> "ジェイソン・ボーヒーズ"

Anonymous化する事でRailsの様にSymbolなのかStringなのか気にする事なくアクセスできる様になります。

すごく良いと思いませんか?
データサイズがでかくなるとパフォーマンスやメモリサイズに影響すると思うので、その場合は別のアプローチを考えた方が良いと思いますが、ちょっとしたデータならクラス定義する事なく便利に使えると思います。

参考までに、AnonymousはiRubyKaig 13で使っていました。クラスとして扱いたかったので、クラス内の内部データとして使ってます。ソースもあります。

たまに#to_objectをするのを忘れてはまる事がありますので、お気をつけ下さい。

最初はNSDictionaryに実装してて、#to_objectも不要だったんですが、基本になるクラスなので後々の事とか考えて別クラスにする事になりました。#method_missingなんてみんな大好きですものね。

最後にGemfileとapp_delegate.rbを載せておきます。

# Gemfile
source 'https://rubygems.org'
gem 'rake'
#gem 'sugarcube', :require => 'sugarcube-all'
gem 'sugarcube', :require => ['sugarcube-anonymous'] # 1つだったら配列にしなくてもいいです。
gem 'bubble-wrap'
app_delegate.rb
# -*- coding: utf-8 -*-
class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)

    @jason_info = {
      name:"Jason Voorhees",
      movies: [
        { title:"Friday the 13th", year:1980 },
        { title:"PART2 Friday the 13th Part 2", year:1981},
        { title:"PART3 Friday the 13th Part 3", year:1983},
        { title:"Friday the 13th : The Final Chapter", year:1984},
        { title:"Friday the 13th Part V : A New Beginning", year:1985},
        { title:"Friday the 13th Part VI : Jason Lives", year: 1986},
        { title:"Friday the 13th Part VII : The New Blood", year: 1988},
        { title:"Friday the 13th Part VIII : Jason Takes Manhattan", year: 1989},
        { title:"Jason Goes to Hell : The Final Friday", year: 1993},
        { title:"Jason X ", year: 2002},
        { title:"FREDDY VS. JASON", year: 2003},
        { title:"Friday the 13th", year: 2009}
      ]
    }

    hash_style_access
    anonymous_style_access

    true
  end
end

# Hashへのアクセスの場合
def hash_style_access
  p @jason_info[:name]
  p @jason_info[:movies].first[:title]
  p @jason_info[:movies].first[:year]

  # Stringのキーではnilになる
  p @jason_info["name"]
end

# Anonymousを使った場合のアクセス
def anonymous_style_access
  # Anonymous化
  jason = @jason_info.to_object
  # 中身のアクセス
  p jason.name
  p jason.movies.first.title
  p jason.movies.first.year

  # 変更
  jason.name = "ジェイソン・ボーヒーズ"
  p jason.name

  # キーが存在しない場合はNoMethodErrorが発生します。
  begin
    jason.category = "Horror"
    raise "It must raise a NoMethodError."
  rescue NoMethodError => e
    p e
  end

  # Hashとして入れてからなら大丈夫
  jason[:category] = nil
  jason.category = "Horror"
  p jason.category


  # シリアライズして再現する場合
  json = BW::JSON.generate jason
  p json
  # ここでファイルに保存したつもり
  # ここでファイルから読み込んだつもり
  revived_jason_info = BW::JSON.parse(json)
  # キーがStringに変化している
  p revived_jason_info

  # Anonymous化する事でRailsの様にSymbolかStringか気にしなくても良くなる
  revived_jason = revived_jason_info.to_object
  p revived_jason.name

  # collectionも大丈夫です。抜けがあるかもしれないのでその時はパッチを書いて下さい。
  movies_in_80s = jason.movies.find_all{|m| (1980..1989).include? m.year}
  p movies_in_80s.size
  p movies_in_80s.first.title
end