57
Ruby中級入門 @shokai 201385() @masuilab

Ruby中級入門

Embed Size (px)

DESCRIPTION

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

Citation preview

Page 1: Ruby中級入門

Ruby中級入門@shokai

2013年8月5日(火)

@masuilab

Page 2: Ruby中級入門

•@shokai (しょうかい)

•趣味:料理、glitch

Page 3: Ruby中級入門

ある程度大きなアプリケーションを作っていると、部品に分割したくなると思います。アプリ内ライブラリやgem

の作り方を説明します。Rubyの機能を活用した使い勝手の良いライブラリのデザインについて考えます。

Page 4: Ruby中級入門

• アプリ内ライブラリの作り方・gemの作り方

• サンプルコードとテスト

• ライブラリのデザイン

• API

• DSL

• 泥臭い小手先の技

• 例外・エラーの通知

• ドキュメント

コンテンツ

Page 5: Ruby中級入門

ライブラリを作る

例:LeapMotionを自作アプリに組み込むためのアプリ内ライブラリを作る

Page 6: Ruby中級入門

• 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に入れる

Page 7: Ruby中級入門

$:, $LOAD_PATH

• requireでファイルを探す場所

• 配列です

• $: と $LOAD_PATH

は同じ

• 配列なので自由に追加できます

Page 8: Ruby中級入門

$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の上のディレクトリを参照する場合

Page 9: Ruby中級入門

クラスメソッドから使わせてる理由

• 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

Page 10: Ruby中級入門

クラスメソッド/変数良いところ

• newしない分だけコードが短くなる

• どこからでもアクセスできる• コールバック関数を引っ掛けるのに良い• 1つだけしか存在しない

• cacheを置くのに便利

• 物理的に1つしか存在しないデバイスを扱う場合

• 例:ORMapper

• query発行はクラスメソッド、返り値はインスタンス

Page 11: Ruby中級入門

クラスメソッド/変数を活用したライブラリ

例:料理のレシピをスクレイピングするSinatraアプリ内で使うライブラリを想定

Page 12: Ruby中級入門

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する

Page 13: Ruby中級入門

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つけるとクラスメソッドになる

Page 14: Ruby中級入門

• __FILE__ は自分自身

• $0 は ruby foo.rb で実行した場合の foo.rb

• ある程度大きくなったらちゃんとテスト書いたほうがいいです

• 引数がおかしいかどうか、調べて例外を投げたほうがいい(ライブラリ利用者が使い方をインタラクティブに学べるし、ドキュメントも減らせる)

Page 15: Ruby中級入門

例外処理unless arg.kind_of? String raise ArgumentError, "引数はStringにしてください"end

組み込み型をraise

module MyApp class YabaiError < StandardError endend

raise MyApp::YabaiError, "マジヤバイです、爆発します"

自分のアプリ用の例外クラスを作る

非同期処理する場合、エラーもコールバックで返すといいかも

変な

puts

残さないでね

Page 16: Ruby中級入門

Rubygemを作る

Page 17: Ruby中級入門

gemを作ると便利• ライブラリを公開する標準的な手段

• 実行コマンドも含められる

• テストも書いて、アプリと独立してメンテすると安心感ある

• rubygems.orgへの登録

• 審査は無い

• どのマシンにも一発インストールできて便利

• rubygems.orgへ登録しない場合も、gemのフォーマットは便利

• Gitリポジトリなら、bundler/Gemfileの書き方次第でGemと同等に扱える

• → http://shokai.org/blog/archives/7262

Page 18: Ruby中級入門

gemのテンプレートを生成• 試しにkazusukeというgemを作る場合

• % gem install bundler

• % bundle gem kazusuke

• テンプレートが生成される。ファイル少なくてシンプル。

Page 19: Ruby中級入門

生成されたgemテンプレート• 最初から全体が git init されている

• kazusuke.gemspec

• gemの名前、概要、webサイト、依存gemなどgemとしてのspecが書かれている

• 一番重要

• Gemfile

• gemの依存関係を記したファイル、しかし「全部gemspec参照」とだけ書かれている

• Rakefile

• Rubyで書けるMakefile。require "bundler/gem_tasks" だけ書かれている

Page 20: Ruby中級入門

gemspec

• spec.add_development_dependency は開発に必要なgem

• 依存gemの追加は spec.add_dependency “gem名”

binディレクトリ下のファイル全てを実行可能と登録

.gemにまとめられて配布されるファイルはgit ls-filesをそのまま使っている

libディレクトリがloadpathに入る

Page 21: Ruby中級入門

生成されたgemテンプレート(2)

• LICENSE.txt

• デフォルトでMITライセンス

• README.md

• gemの使い方を書く。「gem install kazusukeしてください」みたいな定型文は自動生成されている。

• lib/kazusuke.rb

• gemの本体コードを書く。require “kazusuke”で読み込まれるのがコレ

• 自由に書いていい

• lib/kazusuke/version.rb

• gemのバージョン番号だけ書くファイル。

Page 22: Ruby中級入門

libディレクトリの中身

• require “kazusuke” すると lib/kazusuke.rb が読み込まれる

• lib/kazusuke/ ディレクトリ以下で実装し、lib/kazusuke.rbからrequireする

• lib直下に置くとrequireが誤爆しそうで怖いので(試してない)

Page 23: Ruby中級入門

gemの中身を実装する

• gemspecを埋める

• lib/kazusuke.rbを編集

• READMEに使い方を書く

• samplesかexamplesのようなディレクトリを作って、サンプルコードを入れておく

Page 24: Ruby中級入門

サンプルコード

• libディレクトリをloadpathに追加する

• executable (binディレクトリ) も同様

require 'rubygems'$:.unshift File.expand_path "../lib", File.dirname(__FILE__)require 'kazusuke'

kzsk = Kazusuke.new

## (略)

samples/sample.rb

Page 25: Ruby中級入門

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 で公開される

Page 26: Ruby中級入門

ライブラリのデザイン

Page 27: Ruby中級入門

ライブラリのデザイン• ArduinoFirmata gemの場合

• Arduino使ったことある人が即使えるようにしてある

• digitalWrite(pin, state) を digital_write(pin, state) に置き換えるだけでArduinoコードをRubyに翻訳できるようにした

• Skype gemの場合

• Skype APIはかなりこんがらがったRPCなので、オブジェクト指向的に使いにくい

• しかしChat→Messages→Userのように、明らかにオブジェクト間の関係性がある

• こういう場合、理解しやすいモデルで再構成した方がいい

• 気持ちよく書けるようになってれば良いと思う

Page 28: Ruby中級入門

気持よく使えるライブラリを作る

• 先にsample.rbを書く

• わあ簡単便利!と思うようなサンプルコードを書く

• Rubyなら、それを実行できるライブラリは必ず作れる

chat = Skype.chats.find{|c| c.topic =~ /増井研/ }chat.messages.each do |m| puts "#{m.user} #{m.body}"endchat.post "おなかすいた"

Page 29: Ruby中級入門

わかりやすい != 直感的• “直感的”なインタフェースは存在しない

• 今までに使ったことがあるモノに似ているから、理解できるだけ

• 誤操作した時に適切なフィードバックが返ってくるから、理解の速度が高まるだけ

• 既存のライブラリのインタフェースを参考にして、「アレと同じ考え方で使えるよ」と説明する

• エラーメッセージをちゃんと出す

Page 30: Ruby中級入門

ライブラリの使い方を伝える• わかりやすいインタフェース、メンタルモデル

• わかりやすいサンプルコード

• ドキュメント

• テストコード嫁

• の順な気がする

• 概念が斬新すぎて、しっかり理解しないと使えない場合はblogとか書いて啓蒙するしかない

Page 31: Ruby中級入門

見えない所で使う小手先の技

Page 32: Ruby中級入門

[], []=

• Hashや配列風にアクセスできる

class Foo def [](key) "#{key}にアクセスされた" end

def []=(key, value) puts "#{key}に#{value}が書き込まれた" endend

foo = Foo.newfoo[3] = "hoge"puts foo[5]

Page 33: Ruby中級入門

四則演算の上書き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

Page 34: Ruby中級入門

monkeypatchclass String def kensakuyoke self.split(//u).join("/") endend

puts "検索よけ".kensakuyoke # => "検/索/よ/け"

• 既存クラスに機能追加できる

Page 35: Ruby中級入門

引数のデフォルト値class User def initialize(name, age=10) @name = name @age = age endend

user = User.new "shokai", 28user2 = User.new "ahokai"

Page 36: Ruby中級入門

キーワード引数

• 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

Page 37: Ruby中級入門

キーワード引数 (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"

Page 38: Ruby中級入門

環境変数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認証のパスワードとか)

Page 39: Ruby中級入門

キーワード引数やデフォルト値をうまく使うと機能追加や内部実装の変化を隠蔽できると思います

Page 40: Ruby中級入門

||=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回しか実行させたくない処理に使える

Page 41: Ruby中級入門

mix-in

• moduleの関数をclassに埋め込む

module Foo def bar puts "baaaaaaaaaaaaaaa" endend

class Baz include Fooend

Baz.new.bar

Page 42: Ruby中級入門

メタプログラミングを使ったライブラリ実装テクニック

Page 43: Ruby中級入門

% gem install skypeSkype Desktop APIのラッパー

Mac/Linux対応https://github.com/shokai/skype-ruby

method_missingを使用

Page 44: Ruby中級入門

require 'rubygems'require 'skype'

# チャットSkype.message("shokaishokai", "電話かけます")

# 電話Skype.call("shokaishokai")

call関数やmessage関数はgem内に実装されていないしかしなぜか呼び出せる

Page 45: Ruby中級入門

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別々に実装

Page 46: Ruby中級入門

• method_missingはAPIのラッパーを作るのに便利

• Twitter gem等、対象APIに規則性がある場合に有効

• API側が変わっても、gemを修正する必要がない

• 例:”CALL shokaishokai” が “CALLTO shokaishokai” に仕様変更されても、Skype.callto “shokaishokai” が動的に生成されるから問題ない

• Skype APIが変更された場合、新しいAPIのSkypeクライアントと古いクライアントが混在するが、gem側で判別するのは面倒臭い。実際、Linux版とMac版でSkype APIは細かい所が違う

Page 47: Ruby中級入門

% gem install babascriptコンピュータが得意な事はコンピュータが、

人間が得意な事は馬場くん  がやってくれる言語

https://github.com/masuilab/babascript

instance_eval, method_missingを使用

@takumibaba

Page 48: Ruby中級入門

% baba -e 'アイス買ってきて("#{rand 5}本")'

baba -e ’コード’

もしくはbaba ファイル名

Page 49: Ruby中級入門

結果

Page 50: Ruby中級入門

res = かず助に行きたい人の出欠取ってくださいloop do num = res.to_i # 整数に変換 if num > 0 puts では予約してください("#{num}人") exit else res = 残念・・その次の週はどうですか? puts res endend

出欠確認.bb

% baba 出欠確認.bb

実行

ただのRuby…ではなく日本語で書いた部分を馬場君が実行してくれる

Page 51: 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も読める

Page 52: Ruby中級入門

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

Page 53: Ruby中級入門

• instance_eval + method_missingで言語が歪む

• エラーメッセージがわかりにくくなるので、何でもかんでもinstance_evalするとコード追えなくなる

• どこかグローバルな場所でエラーのcallback関数を登録できると追いやすい

• 英語っぽいDSLで書けるgemで使われているはず(rspecなど)

Page 54: Ruby中級入門

人間を関数のように扱えるようになるスマホアプリ

+人間に命令を送る構文を追加したプログラム言語

→ 人間とプログラム言語の新しい関係

Page 55: Ruby中級入門

まとめ• メンタルモデル的に理解しやすいライブラリ仕様にしよう

• 理想:gem install中にドキュメント見てて、インストール終わったらすぐ実装に入れるかんじ。適当にいじっても親切なエラーが返ってきて、まるでプログラムが動くドキュメントだ

• 短く簡潔なインタフェースで使えるようにしよう

• そのためには泥臭い事も厭わない

• 理想:1年後に仕様を忘れた上、泥酔してても使えるぐらい、親切に実装したい

Page 57: Ruby中級入門

おわり質問などあれば @shokai へお気軽にどうぞ