Upload
sho-hashimoto
View
22.328
Download
0
Embed Size (px)
DESCRIPTION
Ruby中級入門という勉強会をやりました http://shokai.org/blog/archives/8091 中級に入門って意味わからないけど。初級はこちらへ http://www.slideshare.net/shokai/130715-ruby-intro
Citation preview
Ruby中級入門@shokai
2013年8月5日(火)
@masuilab
私
•@shokai (しょうかい)
•趣味:料理、glitch
ある程度大きなアプリケーションを作っていると、部品に分割したくなると思います。アプリ内ライブラリやgem
の作り方を説明します。Rubyの機能を活用した使い勝手の良いライブラリのデザインについて考えます。
• アプリ内ライブラリの作り方・gemの作り方
• サンプルコードとテスト
• ライブラリのデザイン
• API
• DSL
• 泥臭い小手先の技
• 例外・エラーの通知
• ドキュメント
コンテンツ
ライブラリを作る
例:LeapMotionを自作アプリに組み込むためのアプリ内ライブラリを作る
• 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 endend
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 dataend
loop do sleep 1end
libsディレクトリをpathに追加クラスメソッドから
使わせる
main.rb
エラーもeventで投げる
アプリ名のmoduleに入れる
$:, $LOAD_PATH
• requireでファイルを探す場所
• 配列です
• $: と $LOAD_PATH
は同じ
• 配列なので自由に追加できます
$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の上のディレクトリを参照する場合
クラスメソッドから使わせてる理由
• 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 endend
クラスメソッド/変数良いところ
• newしない分だけコードが短くなる
• どこからでもアクセスできる• コールバック関数を引っ掛けるのに良い• 1つだけしか存在しない
• cacheを置くのに便利
• 物理的に1つしか存在しないデバイスを扱う場合
• 例:ORMapper
• query発行はクラスメソッド、返り値はインスタンス
クラスメソッド/変数を活用したライブラリ
例:料理のレシピをスクレイピングするSinatraアプリ内で使うライブラリを想定
get %r{^/recipe/([1-9][0-9]+)$} do |recipe_id| @recipe = CookPad.get_recipe recipe_id.to_i haml :recipeend
get %r{^/recipe/([1-9][0-9]+)\.json$} do |recipe_id| CookPad.get_recipe(recipe_id.to_i).to_jsonend
こんな感じでSinatra
で使えるライブラリを作る
• レシピIDで取得
• 一度取得したデータはcacheする
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) endend
if __FILE__ == $0 require 'awesome_print' ap CookPad.get_recipe 12345 ap CookPad.get_recipe 12345 ap CookPad.get_recipe 12343end
自分自身を実行した時のみtrueになるライブラリとしてrequireされた時はfalse
簡単なテストを書く
引数の型をチェックして例外を投げる
@2つはクラス変数
リクエストをキャッシュする
selfつけるとクラスメソッドになる
• __FILE__ は自分自身
• $0 は ruby foo.rb で実行した場合の foo.rb
• ある程度大きくなったらちゃんとテスト書いたほうがいいです
• 引数がおかしいかどうか、調べて例外を投げたほうがいい(ライブラリ利用者が使い方をインタラクティブに学べるし、ドキュメントも減らせる)
例外処理unless arg.kind_of? String raise ArgumentError, "引数はStringにしてください"end
組み込み型をraise
module MyApp class YabaiError < StandardError endend
raise MyApp::YabaiError, "マジヤバイです、爆発します"
自分のアプリ用の例外クラスを作る
非同期処理する場合、エラーもコールバックで返すといいかも
変な
puts
残さないでね
Rubygemを作る
gemを作ると便利• ライブラリを公開する標準的な手段
• 実行コマンドも含められる
• テストも書いて、アプリと独立してメンテすると安心感ある
• rubygems.orgへの登録
• 審査は無い
• どのマシンにも一発インストールできて便利
• rubygems.orgへ登録しない場合も、gemのフォーマットは便利
• Gitリポジトリなら、bundler/Gemfileの書き方次第でGemと同等に扱える
• → http://shokai.org/blog/archives/7262
gemのテンプレートを生成• 試しにkazusukeというgemを作る場合
• % gem install bundler
• % bundle gem kazusuke
• テンプレートが生成される。ファイル少なくてシンプル。
生成されたgemテンプレート• 最初から全体が git init されている
• kazusuke.gemspec
• gemの名前、概要、webサイト、依存gemなどgemとしてのspecが書かれている
• 一番重要
• Gemfile
• gemの依存関係を記したファイル、しかし「全部gemspec参照」とだけ書かれている
• Rakefile
• Rubyで書けるMakefile。require "bundler/gem_tasks" だけ書かれている
gemspec
• spec.add_development_dependency は開発に必要なgem
• 依存gemの追加は spec.add_dependency “gem名”
binディレクトリ下のファイル全てを実行可能と登録
.gemにまとめられて配布されるファイルはgit ls-filesをそのまま使っている
libディレクトリがloadpathに入る
生成されたgemテンプレート(2)
• LICENSE.txt
• デフォルトでMITライセンス
• README.md
• gemの使い方を書く。「gem install kazusukeしてください」みたいな定型文は自動生成されている。
• lib/kazusuke.rb
• gemの本体コードを書く。require “kazusuke”で読み込まれるのがコレ
• 自由に書いていい
• lib/kazusuke/version.rb
• gemのバージョン番号だけ書くファイル。
libディレクトリの中身
• require “kazusuke” すると lib/kazusuke.rb が読み込まれる
• lib/kazusuke/ ディレクトリ以下で実装し、lib/kazusuke.rbからrequireする
• lib直下に置くとrequireが誤爆しそうで怖いので(試してない)
gemの中身を実装する
• gemspecを埋める
• lib/kazusuke.rbを編集
• READMEに使い方を書く
• samplesかexamplesのようなディレクトリを作って、サンプルコードを入れておく
サンプルコード
• libディレクトリをloadpathに追加する
• executable (binディレクトリ) も同様
require 'rubygems'$:.unshift File.expand_path "../lib", File.dirname(__FILE__)require 'kazusuke'
kzsk = Kazusuke.new
## (略)
samples/sample.rb
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 で公開される
ライブラリのデザイン
ライブラリのデザイン• ArduinoFirmata gemの場合
• Arduino使ったことある人が即使えるようにしてある
• digitalWrite(pin, state) を digital_write(pin, state) に置き換えるだけでArduinoコードをRubyに翻訳できるようにした
• Skype gemの場合
• Skype APIはかなりこんがらがったRPCなので、オブジェクト指向的に使いにくい
• しかしChat→Messages→Userのように、明らかにオブジェクト間の関係性がある
• こういう場合、理解しやすいモデルで再構成した方がいい
• 気持ちよく書けるようになってれば良いと思う
気持よく使えるライブラリを作る
• 先にsample.rbを書く
• わあ簡単便利!と思うようなサンプルコードを書く
• Rubyなら、それを実行できるライブラリは必ず作れる
chat = Skype.chats.find{|c| c.topic =~ /増井研/ }chat.messages.each do |m| puts "#{m.user} #{m.body}"endchat.post "おなかすいた"
わかりやすい != 直感的• “直感的”なインタフェースは存在しない
• 今までに使ったことがあるモノに似ているから、理解できるだけ
• 誤操作した時に適切なフィードバックが返ってくるから、理解の速度が高まるだけ
• 既存のライブラリのインタフェースを参考にして、「アレと同じ考え方で使えるよ」と説明する
• エラーメッセージをちゃんと出す
ライブラリの使い方を伝える• わかりやすいインタフェース、メンタルモデル
• わかりやすいサンプルコード
• ドキュメント
• テストコード嫁
• の順な気がする
• 概念が斬新すぎて、しっかり理解しないと使えない場合はblogとか書いて啓蒙するしかない
見えない所で使う小手先の技
[], []=
• Hashや配列風にアクセスできる
class Foo def [](key) "#{key}にアクセスされた" end
def []=(key, value) puts "#{key}に#{value}が書き込まれた" endend
foo = Foo.newfoo[3] = "hoge"puts foo[5]
四則演算の上書きclass Foo def +(value) "#{value}が足された" end
def -(value) "#{value}が引かれた" end
def *(value) "#{value} がかけられた" end
def /(value) "#{value} で割られた" endend
foo = Foo.newfoo2 = Foo.new
puts foo + foo2puts foo * foo2puts foo / foo2
monkeypatchclass String def kensakuyoke self.split(//u).join("/") endend
puts "検索よけ".kensakuyoke # => "検/索/よ/け"
• 既存クラスに機能追加できる
引数のデフォルト値class User def initialize(name, age=10) @name = name @age = age endend
user = User.new "shokai", 28user2 = User.new "ahokai"
キーワード引数
• ruby2.0から使えるようになった
• 自由な順番で引数を渡せる
• 実はHashでも引数渡せる
class User def initialize(name: "shokai", age: 10) @name = name @age = age endend
user = User.new age: 28, name: "shokai"user2 = User.new name: "ahokai"user3 = User.new :name => "kazusuke", :age => 123
キーワード引数 (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] endend
user = User.new :age => 28, :name => "shokai"user2 = User.new :name => "ahokai"
環境変数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認証のパスワードとか)
キーワード引数やデフォルト値をうまく使うと機能追加や内部実装の変化を隠蔽できると思います
||=require "httparty"
class CachedHttpGet @@cache = {}
def self.get(url) @@cache[url] ||= HTTParty.get(url).response.body endend
puts CachedHttpGet.get "http://shokai.org"puts CachedHttpGet.get "http://shokai.org"
• オアイコール• 左辺がfalseやnilの時、左辺に右辺が代入される
• 1回しか実行させたくない処理に使える
mix-in
• moduleの関数をclassに埋め込む
module Foo def bar puts "baaaaaaaaaaaaaaa" endend
class Baz include Fooend
Baz.new.bar
メタプログラミングを使ったライブラリ実装テクニック
% gem install skypeSkype Desktop APIのラッパー
Mac/Linux対応https://github.com/shokai/skype-ruby
method_missingを使用
require 'rubygems'require 'skype'
# チャットSkype.message("shokaishokai", "電話かけます")
# 電話Skype.call("shokaishokai")
call関数やmessage関数はgem内に実装されていないしかしなぜか呼び出せる
module Skype def self.method_missing(name, *args) self.exec "#{name.upcase} #{args.join(' ')}" endend
method_missing
Skype Desktop API
"CALL shokaishokai""MESSAGE shokaishokai こんにちは"
Query文字列をSkype.appに送るだけの簡単仕様
実装されていない関数の呼び出しを受け取る関数
Skype.execの中身はMac/Linux別々に実装
• method_missingはAPIのラッパーを作るのに便利
• Twitter gem等、対象APIに規則性がある場合に有効
• API側が変わっても、gemを修正する必要がない
• 例:”CALL shokaishokai” が “CALLTO shokaishokai” に仕様変更されても、Skype.callto “shokaishokai” が動的に生成されるから問題ない
• Skype APIが変更された場合、新しいAPIのSkypeクライアントと古いクライアントが混在するが、gem側で判別するのは面倒臭い。実際、Linux版とMac版でSkype APIは細かい所が違う
% gem install babascriptコンピュータが得意な事はコンピュータが、
人間が得意な事は馬場くん がやってくれる言語
https://github.com/masuilab/babascript
instance_eval, method_missingを使用
@takumibaba
% baba -e 'アイス買ってきて("#{rand 5}本")'
baba -e ’コード’
もしくはbaba ファイル名
結果
res = かず助に行きたい人の出欠取ってくださいloop do num = res.to_i # 整数に変換 if num > 0 puts では予約してください("#{num}人") exit else res = 残念・・その次の週はどうですか? puts res endend
出欠確認.bb
% baba 出欠確認.bb
実行
ただのRuby…ではなく日本語で書いた部分を馬場君が実行してくれる
class Foo def initialize @name = "bar" endend
foo = Foo.newfoo.instance_eval do puts @name # => "bar"end
instance_evalとは
babaコマンドの中身 = instance_evalFile.open(fname) do |f| BabaScript::Baba.instance_eval f.readend
コードやブロックをそのインスタンスのコンテキストで実行する
fooのコンテキストで実行されるのでアクセサが無いプロパティ@nameも読める
module BabaScript class Baba
def self.method_missing(name, *args) ## (略) AndroidにnameとargsをLindaで送信する end
endend
Rubyの関数名には日本語が使える→ method_missingで全部取れる
File.open(fname) do |f| BabaScript::Baba.instance_eval f.readend
• instance_eval + method_missingで言語が歪む
• エラーメッセージがわかりにくくなるので、何でもかんでもinstance_evalするとコード追えなくなる
• どこかグローバルな場所でエラーのcallback関数を登録できると追いやすい
• 英語っぽいDSLで書けるgemで使われているはず(rspecなど)
人間を関数のように扱えるようになるスマホアプリ
+人間に命令を送る構文を追加したプログラム言語
→ 人間とプログラム言語の新しい関係
まとめ• メンタルモデル的に理解しやすいライブラリ仕様にしよう
• 理想:gem install中にドキュメント見てて、インストール終わったらすぐ実装に入れるかんじ。適当にいじっても親切なエラーが返ってきて、まるでプログラムが動くドキュメントだ
• 短く簡潔なインタフェースで使えるようにしよう
• そのためには泥臭い事も厭わない
• 理想:1年後に仕様を忘れた上、泥酔してても使えるぐらい、親切に実装したい
より詳しく学びたくなったら
おわり質問などあれば @shokai へお気軽にどうぞ