Ruby中級入門

25,016 views

Published on

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

Published in: Technology
0 Comments
96 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
25,016
On SlideShare
0
From Embeds
0
Number of Embeds
2,711
Actions
Shares
0
Downloads
194
Comments
0
Likes
96
Embeds 0
No embeds

No notes for slide

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 へお気軽にどうぞ

×