Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Ruby中級入門

27,714 views

Published on

Ruby中級入門という勉強会をやりました http://shokai.org/blog/archives/8091 中級に入門って意味わからないけど。初級はこちらへ http://www.slideshare.net/shokai/130715-ruby-intro

Published in: Technology

Ruby中級入門

  1. 1. Ruby中級入門 @shokai 2013年8月5日(火) @masuilab
  2. 2. 私 •@shokai (しょうかい) •趣味:料理、glitch
  3. 3. ある程度大きなアプリケーションを作 っていると、部品に分割したくなると 思います。アプリ内ライブラリやgem の作り方を説明します。Rubyの機能を 活用した使い勝手の良いライブラリの デザインについて考えます。
  4. 4. • アプリ内ライブラリの作り方・gemの作り方 • サンプルコードとテスト • ライブラリのデザイン • API • DSL • 泥臭い小手先の技 • 例外・エラーの通知 • ドキュメント コンテンツ
  5. 5. ライブラリを作る 例:LeapMotionを自作アプリに組み込むための アプリ内ライブラリを作る
  6. 6. • LeapMotionはport 6437にWebSocket 接続するとJSONで読める • 別スレッドで非同期受信するの で、データはeventで返した require "json" require "event_emitter" require "websocket-client-simple" module MyApp class LeapMotion def self.connect(ws_url="ws://localhost:6437") MyApp::LeapMotion::Device.new ws_url end class Device include EventEmitter def initialize(ws_url) @ws = WebSocket::Client::Simple.connect ws_url this = self @ws.on :message do |msg| begin data = JSON.parse msg.data rescue this.emit :error, "JSON parse error" end if data.has_key? "currentFrameRate" this.emit :data, data end end @ws.on :open do this.emit :connect end end end end end libs/leapmotion.rb require "rubygems" $:.unshift File.expand_path "libs", File.dirname(__FILE__) require "leapmotion" leap = MyApp::LeapMotion.connect leap.on :connect do puts "leap connected!" end leap.on :data do |data| p data end loop do sleep 1 end libsディレクトリをpathに追加 クラスメソッドから 使わせる main.rb エラーもeventで投げる アプリ名のmoduleに入れる
  7. 7. $:, $LOAD_PATH • requireでファイル を探す場所 • 配列です • $: と $LOAD_PATH は同じ • 配列なので自由に 追加できます
  8. 8. $LOAD_PATHの追加 ├─ libs │   └─ leapmotion.rb └─ main.rb $LOAD_PATH.unshift File.expand_path "libs", File.dirname(__FILE__) ├─ libs │   └─ leapmotion.rb └─ bin    └─ main.rb $LOAD_PATH.unshift File.expand_path "../libs", File.dirname(__FILE__) loadpathは配列なので Array#unshiftで先頭に追加できる binの上のディレクトリ を参照する場合
  9. 9. クラスメソッドから使わせてる理由 • LeapMotion::Device.newではなくLeapMotion.connectの方が自然 • 物理的に1つしか存在しないデバイスなのにDevice.newってどうなの • Device.newしただけで自動的に接続されるの?されないの? • DeviceではなくLeapMotion::DeviceConnectionというクラスにすればいい気も するけど、長すぎるの嫌。 • 名前重要、覚えやすくて意味わかりやすい方がいい module MyApp class LeapMotion def self.connect(ws_url="ws://localhost:6437") MyApp::LeapMotion::Device.new ws_url end class Device def initialize(ws_url) @ws = WebSocket::Client::Simple.connect ws_url end ## (略) end end end
  10. 10. クラスメソッド/変数 良いところ • newしない分だけコードが短くなる • どこからでもアクセスできる • コールバック関数を引っ掛けるのに良い • 1つだけしか存在しない • cacheを置くのに便利 • 物理的に1つしか存在しないデバイスを扱う場合 • 例:ORMapper • query発行はクラスメソッド、返り値はインスタンス
  11. 11. クラスメソッド/変数を 活用したライブラリ 例:料理のレシピをスクレイピングする Sinatraアプリ内で使うライブラリを想定
  12. 12. get %r{^/recipe/([1-9][0-9]+)$} do |recipe_id| @recipe = CookPad.get_recipe recipe_id.to_i haml :recipe end get %r{^/recipe/([1-9][0-9]+).json$} do |recipe_id| CookPad.get_recipe(recipe_id.to_i).to_json end こんな感じでSinatra で使えるライブラリを作る • レシピIDで取得 • 一度取得したデータはcacheする
  13. 13. require 'hashie' require 'nokogiri' require 'httparty' class CookPad class Recipe < Hashie::Mash end @@base_url = "http://cookpad.com" @@cache = {} def self.get_recipe(id) raise ArgumentError, "ID must be Fixnum" unless id.kind_of? Fixnum if @@cache.has_key? id return @@cache[id] end doc = Nokogiri::HTML.parse HTTParty.get("#{@@base_url}/recipe/#{id}").response.body ingredients = {} doc.xpath('//*[@class="ingredient_row"]').each do |row| ingredients[ row.children[1].text ] = row.children[3].text end steps = doc.xpath('//*[@id="steps"]//p').map{|step| step.text.strip } @@cache[id] = Recipe.new(:title => doc.xpath('//h1').text.strip, :ingredients => ingredients, :steps => steps) end end if __FILE__ == $0 require 'awesome_print' ap CookPad.get_recipe 12345 ap CookPad.get_recipe 12345 ap CookPad.get_recipe 12343 end 自分自身を実行した時のみtrueになる ライブラリとしてrequireされた時はfalse 簡単なテストを書く 引数の型をチェック して例外を投げる @2つはクラス変数 リクエストをキャッシュする selfつけるとクラスメソッドになる
  14. 14. • __FILE__ は自分自身 • $0 は ruby foo.rb で実行した場合の foo.rb • ある程度大きくなったらちゃんとテスト書 いたほうがいいです • 引数がおかしいかどうか、調べて例外を投 げたほうがいい(ライブラリ利用者が使い 方をインタラクティブに学べるし、ドキュ メントも減らせる)
  15. 15. 例外処理 unless arg.kind_of? String raise ArgumentError, "引数はStringにしてください" end 組み込み型をraise module MyApp class YabaiError < StandardError end end raise MyApp::YabaiError, "マジヤバイです、爆発します" 自分のアプリ用の例外クラスを作る 非同期処理する場合、エラーも コールバックで返すといいかも 変 な puts 残 さ な い で ね
  16. 16. Rubygemを作る
  17. 17. gemを作ると便利 • ライブラリを公開する標準的な手段 • 実行コマンドも含められる • テストも書いて、アプリと独立してメンテすると安心感ある • rubygems.orgへの登録 • 審査は無い • どのマシンにも一発インストールできて便利 • rubygems.orgへ登録しない場合も、gemのフォーマットは便利 • Gitリポジトリなら、bundler/Gemfileの書き方次第でGemと同等に扱える • → http://shokai.org/blog/archives/7262
  18. 18. gemのテンプレートを生成 • 試しにkazusukeというgemを作る場合 • % gem install bundler • % bundle gem kazusuke • テンプレートが生成される。ファイル少なくてシンプル。
  19. 19. 生成されたgemテンプレート • 最初から全体が git init されている • kazusuke.gemspec • gemの名前、概要、webサイト、依存gemなどgemとしてのspecが書か れている • 一番重要 • Gemfile • gemの依存関係を記したファイル、しかし「全部gemspec参照」とだ け書かれている • Rakefile • Rubyで書けるMakefile。require "bundler/gem_tasks" だけ書かれている
  20. 20. gemspec • spec.add_development_dependency は開発に必要なgem • 依存gemの追加は spec.add_dependency “gem名” binディレクトリ 下のファイル全て を実行可能と登録 .gemにまとめられ て配布されるファイ ルはgit ls-filesをそ のまま使っている libディレクトリがloadpathに入る
  21. 21. 生成されたgemテンプレート(2) • LICENSE.txt • デフォルトでMITライセンス • README.md • gemの使い方を書く。「gem install kazusukeしてください」みたいな定 型文は自動生成されている。 • lib/kazusuke.rb • gemの本体コードを書く。require “kazusuke”で読み込まれるのがコレ • 自由に書いていい • lib/kazusuke/version.rb • gemのバージョン番号だけ書くファイル。
  22. 22. libディレクトリの中身 • require “kazusuke” すると lib/kazusuke.rb が読み込まれる • lib/kazusuke/ ディレクトリ以下で実装し、lib/kazusuke.rbか らrequireする • lib直下に置くとrequireが誤爆しそうで怖いので(試してない)
  23. 23. gemの中身を実装する • gemspecを埋める • lib/kazusuke.rbを編集 • READMEに使い方を書く • samplesかexamplesのようなディレクトリを 作って、サンプルコードを入れておく
  24. 24. サンプルコード • libディレクトリをloadpathに追加する • executable (binディレクトリ) も同様 require 'rubygems' $:.unshift File.expand_path "../lib", File.dirname(__FILE__) require 'kazusuke' kzsk = Kazusuke.new ## (略) samples/sample.rb
  25. 25. gemを公開する • % git commit -m “release v0.0.1” • % rake install • 自分のマシンにインストールして使ってみる • 動かない場合:git add 忘れでgemに含まれてないファイ ルがある、lib内のrequireのパスがおかしい、等 • % rake release • バージョンでgit tagが打たれ、git pushとgem pushされる • http://rubygems.org/gems/GEM_NAME で公開される
  26. 26. ライブラリのデザイン
  27. 27. ライブラリのデザイン • ArduinoFirmata gemの場合 • Arduino使ったことある人が即使えるようにしてある • digitalWrite(pin, state) を digital_write(pin, state) に置き換える だけでArduinoコードをRubyに翻訳できるようにした • Skype gemの場合 • Skype APIはかなりこんがらがったRPCなので、オブジェクト指向的に 使いにくい • しかしChat→Messages→Userのように、明らかにオブジェクト間の関 係性がある • こういう場合、理解しやすいモデルで再構成した方がいい • 気持ちよく書けるようになってれば良いと思う
  28. 28. 気持よく使えるライブラリを作る • 先にsample.rbを書く • わあ簡単便利!と思うようなサンプルコードを書く • Rubyなら、それを実行できるライブラリは必ず作れる chat = Skype.chats.find{|c| c.topic =~ /増井研/ } chat.messages.each do |m| puts "#{m.user} #{m.body}" end chat.post "おなかすいた"
  29. 29. わかりやすい != 直感的 • “直感的”なインタフェースは存在しない • 今までに使ったことがあるモノに似ている から、理解できるだけ • 誤操作した時に適切なフィードバックが返 ってくるから、理解の速度が高まるだけ • 既存のライブラリのインタフェースを参考にし て、「アレと同じ考え方で使えるよ」と説明する • エラーメッセージをちゃんと出す
  30. 30. ライブラリの使い方を伝える • わかりやすいインタフェース、メンタルモデル • わかりやすいサンプルコード • ドキュメント • テストコード嫁 • の順な気がする • 概念が斬新すぎて、しっかり理解しないと使え ない場合はblogとか書いて啓蒙するしかない
  31. 31. 見えない所で使う 小手先の技
  32. 32. [], []= • Hashや配列風にアクセスできる class Foo def [](key) "#{key}にアクセスされた" end def []=(key, value) puts "#{key}に#{value}が書き込まれた" end end foo = Foo.new foo[3] = "hoge" puts foo[5]
  33. 33. 四則演算の上書き class Foo def +(value) "#{value}が足された" end def -(value) "#{value}が引かれた" end def *(value) "#{value} がかけられた" end def /(value) "#{value} で割られた" end end foo = Foo.new foo2 = Foo.new puts foo + foo2 puts foo * foo2 puts foo / foo2
  34. 34. monkeypatch class String def kensakuyoke self.split(//u).join("/") end end puts "検索よけ".kensakuyoke # => "検/索/よ/け" • 既存クラスに機能追加できる
  35. 35. 引数のデフォルト値 class User def initialize(name, age=10) @name = name @age = age end end user = User.new "shokai", 28 user2 = User.new "ahokai"
  36. 36. キーワード引数 • ruby2.0から使えるようになった • 自由な順番で引数を渡せる • 実はHashでも引数渡せる class User def initialize(name: "shokai", age: 10) @name = name @age = age end end user = User.new age: 28, name: "shokai" user2 = User.new name: "ahokai" user3 = User.new :name => "kazusuke", :age => 123
  37. 37. キーワード引数 (Hashで) class User DEFAULT_OPTIONS = { :name => "shokai", :age => 10 } def initialize(options={}) DEFAULT_OPTIONS.each do |k,v| options[k] = v unless options.has_key? k end @name = options[:name] @age = options[:age] end end user = User.new :age => 28, :name => "shokai" user2 = User.new :name => "ahokai"
  38. 38. 環境変数 url = (ENV["WS_URL"] || "ws://localhost:6437") leap = LeapMotion.connect :websocket => url • 環境変数を設定してから実行 • % export WS_URL=ws://masuilab.org:6437 • % ruby main.rb • ENV[key] で環境変数が取れる • ファイルに残したくない情報をプログラムに渡すの に便利(webアプリのbasic認証のパスワードとか)
  39. 39. キーワード引数やデ フォルト値をうまく 使うと機能追加や内 部実装の変化を隠 できると思います
  40. 40. ||= require "httparty" class CachedHttpGet @@cache = {} def self.get(url) @@cache[url] ||= HTTParty.get(url).response.body end end puts CachedHttpGet.get "http://shokai.org" puts CachedHttpGet.get "http://shokai.org" • オアイコール • 左辺がfalseやnilの時、左辺に右辺が代入される • 1回しか実行させたくない処理に使える
  41. 41. mix-in • moduleの関数をclassに埋め込む module Foo def bar puts "baaaaaaaaaaaaaaa" end end class Baz include Foo end Baz.new.bar
  42. 42. メタプログラミング を使ったライブラリ 実装テクニック
  43. 43. % gem install skype Skype Desktop APIのラッパー Mac/Linux対応 https://github.com/shokai/skype-ruby method_missingを使用
  44. 44. require 'rubygems' require 'skype' # チャット Skype.message("shokaishokai", "電話かけます") # 電話 Skype.call("shokaishokai") call関数やmessage関数はgem内に実装されていない しかしなぜか呼び出せる
  45. 45. module Skype def self.method_missing(name, *args) self.exec "#{name.upcase} #{args.join(' ')}" end end method_missing Skype Desktop API "CALL shokaishokai" "MESSAGE shokaishokai こんにちは" Query文字列をSkype.appに送るだけの簡単仕様 実装されていない関数の呼び出しを受け取る関数 Skype.execの中身はMac/Linux別々に実装
  46. 46. • method_missingはAPIのラッパーを作るのに便利 • Twitter gem等、対象APIに規則性がある場合に有効 • API側が変わっても、gemを修正する必要がない • 例:”CALL shokaishokai” が “CALLTO shokaishokai” に仕様変 更されても、Skype.callto “shokaishokai” が動的に生成される から問題ない • Skype APIが変更された場合、新しいAPIのSkypeクライアント と古いクライアントが混在するが、gem側で判別するのは面 倒臭い。実際、Linux版とMac版でSkype APIは細かい所が違う
  47. 47. % gem install babascript コンピュータが得意な事はコンピュータが、 人間が得意な事は馬場くん  がやってくれる言語 https://github.com/masuilab/babascript instance_eval, method_missingを使用 @takumibaba
  48. 48. % baba -e 'アイス買ってきて("#{rand 5}本")' baba -e ’コード’ もしくは baba ファイル名
  49. 49. 結果
  50. 50. res = かず助に行きたい人の出欠取ってください loop do num = res.to_i # 整数に変換 if num > 0 puts では予約してください("#{num}人") exit else res = 残念・・その次の週はどうですか? puts res end end 出欠確認.bb % baba 出欠確認.bb 実行 ただのRuby…ではなく 日本語で書いた部分を馬場君が実行してくれる
  51. 51. class Foo def initialize @name = "bar" end end foo = Foo.new foo.instance_eval do puts @name # => "bar" end instance_evalとは babaコマンドの中身 = instance_eval File.open(fname) do |f| BabaScript::Baba.instance_eval f.read end コードやブロックをそのインスタンスのコンテキストで実行する fooのコンテキストで実行されるので アクセサが無いプロパティ@nameも読める
  52. 52. module BabaScript class Baba def self.method_missing(name, *args) ## (略) AndroidにnameとargsをLindaで送信する end end end Rubyの関数名には日本語が使える → method_missingで全部取れる File.open(fname) do |f| BabaScript::Baba.instance_eval f.read end
  53. 53. • instance_eval + method_missingで言語が歪む • エラーメッセージがわかりにくくなるの で、何でもかんでもinstance_evalするとコー ド追えなくなる • どこかグローバルな場所でエラーの callback関数を登録できると追いやすい • 英語っぽいDSLで書けるgemで使われている はず(rspecなど)
  54. 54. 人間を関数のように扱える ようになるスマホアプリ + 人間に命令を送る構文を追 加したプログラム言語 → 人間とプログラム言語の 新しい関係
  55. 55. まとめ • メンタルモデル的に理解しやすいライブラリ仕様にしよう • 理想:gem install中にドキュメント見てて、インストール終 わったらすぐ実装に入れるかんじ。適当にいじっても親切 なエラーが返ってきて、まるでプログラムが動くドキュメ ントだ • 短く簡潔なインタフェースで使えるようにしよう • そのためには泥臭い事も厭わない • 理想:1年後に仕様を忘れた上、泥酔してても使えるぐら い、親切に実装したい
  56. 56. より詳しく学びたくなったら
  57. 57. おわり 質問などあれば @shokai へお気軽にどうぞ

×