Rubyの強力なREPLであるpryの基本的な使い方と、RubyのREPL駆動開発について記します。
REPL駆動開発とは、REPLを中心とする開発スタイルのことです。REPL駆動でない一般的な開発フローは、
- プロジェクトのソースコードを編集する
- 実行可能な段階になったら、開発環境にデプロイするなどして動作を確認する
- 上手くいかないところを直す
- 1.に戻る
ですが、2.の段階まで仕上げるのは結構時間がかかり、その間に間違いも犯しやすいです。一方、REPLを開発の中心に据えると、コードを手軽に実行でき、内部の状態も把握しやすいため、コーディングのフィードバックが非常に早く得られます。
pryのインストール
pryをインストールするには、以下のコマンドを実行します。
$ gem install pry pry-doc
MacやLinuxで、ルート権限でない場合は、以下のコマンドを実行します。
$ sudo gem intall pry pry-doc
pryは本体。pry-docがあると、Ruby組み込みのクラスやメソッドのドキュメントやソースコードを見ることができます。
インストールできたらpryを起動します。
$ pry
[1] pry(main)>
少し触ってみると、まずirbとは異なり、シンタックスハイライトや、Tabによる入力補完が効くことが分かります。
コードを書いてファイルに保存する
とりあえず、適当なコードを書いてみます。
[1] pry(main)> def fizzbuzz(num)
[1] pry(main)* 1.upto(num) do |n|
[1] pry(main)* if n % 3 == 0
[1] pry(main)* print "Fizz\n"
[1] pry(main)* elsif n % 5 == 0
[1] pry(main)* print "Buzz\n"
[1] pry(main)* elsif n % 15 == 0
[1] pry(main)* print "FizzBuzz\n"
[1] pry(main)* else
[1] pry(main)* print n.to_s + "\n"
[1] pry(main)* end
[1] pry(main)* end
[1] pry(main)* end
=> :fizzbuzz
[2] pry(main)> fizzbuzz(15)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
Fizz
=> 1
[3] pry(main)>
3の倍数ならFizz
に、5の倍数ならBuzz
に、15の倍数ならFizzBuzz
に変換して、1から15までの整数を出力するコードです。でも、何かおかしいですね。15がFizzBuzz
ではなく、Fizz
になっています。15は3の倍数でもあるから、3つ目の条件n % 15 == 0
よりも先に、1つ目の条件n % 3 == 0
にマッチしてしまったのが原因です。
fizzbuzz
メソッド を書き直さなければいけません。しかし、また13行もタイプするのは面倒です。edit
コマンドを使えば、指定したメソッドをエディタで編集できます。
[3] pry(main)>edit fizzbuzz
起動したエディタで、メソッドの内容を以下のように修正し、保存してエディタを閉じれば、pryのセッションに戻ります。
def fizzbuzz(num)
1.upto(num) do |n|
if n % 15 == 0
print "FizzBuzz\n"
elsif n % 3 == 0
print "Fizz\n"
elsif n % 5 == 0
print "Buzz\n"
else
print n.to_s + "\n"
end
end
end
修正したfizzbuzz
メソッドをREPLで実行してみます。
[4] pry(main)> fizzbuzz(15)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
=> 1
今度は上手くいきました。
ちなみに、edit
コマンドで立ち上がるエディタはデフォルトでは、MacやLinuxではnano、Windowsではnotepadだと思いますが、pryの設定ファイルで変更できます。pryの設定ファイルは、~/.pryrc
、エディタのプロパティは、Pry.editor
です。したがって、たとえばエディタをvimに変更したければ、以下のようにします。
$ echo "Pry.editor = 'vim'" >> ~/.pryrc
'vim'
の部分は、'emacs'
でも'code'
でも、PATHの通ったディレクトリにある任意のエディタを指定して下さい。
プログラムが完成したので、このメソッドはファイルに保存します。ファイルに保存するには、save-file {メソッド名} --to {ファイル名}
を実行します。たとえば、fizzbuzz
メソッドをカレントディレクトリのfizzbuzz.rb
に保存したければ、次のようにします。
[5] pry(main)>save-file fizzbuzz --to './fizzbuzz.rb'
新たにpryを起動する時に、既存のファイルを読み込む場合は、-r
オプションを使います。たとえば、fizzbuzz/rb
を読み込んでpryを起動したければ、以下のようにします。
$ pry -r "./fizzbuzz.rb"
コードをデバッグする
続いて、もう少し複雑なプログラムを書いてみます。バブルソートを書くことにします。隣り合う要素を比較し、昇順になっていなければ順番を入れ替えるアルゴリズムです。
[6] pry(main)> def bubble_sort(nums)
[6] pry(main)* right_end = nums.length - 1
[6] pry(main)* while right_end > 0
[6] pry(main)* (0..right_end).each do |i|
[6] pry(main)* if nums[i] > nums[i + 1]
[6] pry(main)* nums[i], nums[i + 1] = nums[i + 1], nums[i]
[6] pry(main)* end
[6] pry(main)* end
[6] pry(main)* right_end -= 1
[6] pry(main)* end
[6] pry(main)* return nums
[6] pry(main)* end
=> :bubble_sort
[7] pry(main)> bubble_sort([3, 2, 1])
ArgumentError: comparison of Integer with nil failed
from (pry):5:in `>'
[8] pry(main)>
エラーが出てしまいました。Integerとnilを比較することはできないと言われています。例外がスローされているのは5行目ですから、i
が何れかの値の場合に、num[i]
またはnum[i + 1]
がnil
となり、それを比較しようとしたのが原因です。エラーの原因を突き止め、修正します。エディタでbubble_sort
を編集します。
[9] pry(main)>edit bubble_sort
エラーが発生するi
の値を確かめるために、以下のコードを挿入します。
def bubble_sort(nums)
right_end = nums.length - 1
while right_end > 0
(0..right_end).each do |i|
binding.pry if nums[i].nil? || nums[i + 1].nil? # <= 挿入したコード
if nums[i] > nums[i + 1]
nums[i], nums[i + 1] = nums[i + 1], nums[i]
end
end
right_end -= 1
end
return nums
end
binding.pry
がRuby処理系に評価されると、そのコンテクストでpryが起動します。どういうことかというと、こういうことです。
[10] pry(main)> bubble_sort([3, 2, 1])
From: /XXXXXXXX/pry-redefined(0x3fdb8dc62bec#bubble_sort):5 Object#bubble_sort:
1: def bubble_sort(nums)
2: right_end = nums.length - 1
3: while right_end > 0
4: (0..right_end).each do |i|
=> 5: binding.pry if nums[i].nil? || nums[i + 1].nil?
6: if nums[i] > nums[i + 1]
7: nums[i], nums[i + 1] = nums[i + 1], nums[i]
8: end
9: end
10: right_end -= 1
11: end
12: return nums
13: end
[1] pry(main)>
bubble_sort
の5行目を実行した時点で時が止まっています。この状態では以下のように、このスコープ内の変数などをREPLで評価することができます。
[1] pry(main)> i
=> 2
[2] pry(main)> nums[i].nil?
=> false
[3] pry(main)> nums[i + 1].nil?
=> true
[4] pry(main)> nums.length
=> 3
[5] pry(main)>
エラーが出る直前の変数を調べて判ったことは、i
が2のとき、i + 1
が配列の境界外を指しているため、nums[i + 1]
がnil
になってしまったということです。つまり、nums
のi
番目とi + 1
番目を参照するのだから、「i
が0からright_end
」までではなく、「i + 1
が1からright_end
まで、すなわちi
は0からright_end - 1
まで」の範囲を動かなければいけないということです。したがって、そのように修正します。
コードを再実行するには、exit
を実行します。
[5] pry(main)> exit
元のコンテクストに戻ってきたら、edit bubble_sort
でコードを修正します。
def bubble_sort(nums)
right_end = nums.length - 1
while right_end > 0
(0..(right_end - 1)).each do |i| # <= right_end を right_end - 1 に
# binding.pry を削除
if nums[i] > nums[i + 1]
nums[i], nums[i + 1] = nums[i + 1], nums[i]
end
end
right_end -= 1
end
return nums
end
編集内容を保存して、pryのセッションに戻り、修正を確認します。
[10] pry(main)> bubble_sort([3, 2, 1])
=> [1, 2, 3]
[11] pry(main)> bubble_sort([3, 2])
=> [2, 3]
[12] pry(main)> bubble_sort([3])
=> [3]
[13] pry(main)> bubble_sort([])
=> []
[14] pry(main)>
修正が確認できましたので、ソースコードを保存します。
[15] pry(main)>save-file bubble_sort --to './bubble_sort.rb'
上の例では、変数i
の値を参照しただけでしたが、もちろん通常REPLで行うのと同じように、ローカル変数に値を再代入してからコードを再実行することもできます。
[1] pry(main)> def add_tax(price)
[1] pry(main)* tax = 1.08
[1] pry(main)* binding.pry
[1] pry(main)* return (price * tax).to_i
[1] pry(main)* end
=> :add_tax
[2] pry(main)> add_tax(1000)
From: (pry):3 Object#add_tax:
1: def add_tax(price)
2: tax = 1.08
=> 3: binding.pry
4: return (price * tax).to_i
5: end
[1] pry(main)> tax = 1.10
=> 1.1
[2] pry(main)> exit
=> 1100
[3] pry(main)>
このように、pryを使うことでコードの検証や修正が非常に迅速に行えます。
コンテクストを移動する
コンテクストとは、今どのオブジェクトやメソッドの内部にいるのかという情報です。コンテクストを移動すると、そのオブジェクトのインスタンス変数やメソッドのローカル変数を、参照したり変更したりできます。
例として、FIFOのデータ構造を表すクラスQueueと、そのインスタンスを2つ作成します。
[16] pry(main)* def initialize(initial_list)
[16] pry(main)* @queue = initial_list
[16] pry(main)* end
[16] pry(main)*
[16] pry(main)* attr_reader :queue
[16] pry(main)*
[16] pry(main)* def enqueue(value)
[16] pry(main)* @queue.push(value)
[16] pry(main)* end
[16] pry(main)*
[16] pry(main)* def dequeue()
[16] pry(main)* @queue.shift
[16] pry(main)* end
[16] pry(main)* end
=> :dequeue
[17] pry(main)> q1 = Queue.new([])
=> #<Thread::Queue:0x00007fa61bf0b4f0 @queue=[]>
[18] pry(main)> q2 = Queue.new([])
=> #<Thread::Queue:0x00007fa617f08240 @queue=[]>
[19] pry(main)> q1.enqueue(1)
=> [1]
[20] pry(main)> q2.enqueue(2)
=> [2]
[21] pry(main)> q2.enqueue(3)
=> [2, 3]
[22] pry(main)>
コンテクストを移動するには、cd {移動先のオブジェクト}
を実行します。たとえば、先程作成したq1
に移動するには、以下のようにします。
[22] pry(main)> cd q1
[23] pry(#<Thread::Queue>):1>
ls
で、現在のコンテクストで参照可能なオブジェクトを一覧できます。
[23] pry(#<Thread::Queue>):1> ls
Thread::Queue#methods: << clear close closed? deq dequeue empty? enq enqueue initial_list length marshal_dump num_waiting pop push shift size
self.methods: __pry__
instance variables: @queue
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ pry_instance
[24] pry(#<Thread::Queue>):1> @queue
=> [1]
[25] pry(#<Thread::Queue>):1>
現在のコンテクストから抜けて、元のコンテクストに戻るには、exit
を実行します。
[25] pry(#<Thread::Queue>):1> exit
=> #<Thread::Queue:0x00007fa72c80ca48 @queue=[1]>
同様に、q2
のコンテクストも覗いてみると、インスタンス変数@queue
の値が異なることが確認できます。
[26] pry(main)> cd q2
[27] pry(#<Thread::Queue>):1> ls
Thread::Queue#methods: << clear close closed? deq dequeue empty? enq enqueue initial_list length marshal_dump num_waiting pop push shift size
self.methods: __pry__
instance variables: @queue
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ pry_instance
[28] pry(#<Thread::Queue>):1> @queue
=> [2, 3]
[29] pry(#<Thread::Queue>):1> exit
=> #<Thread::Queue:0x00007fa72429c4d0 @queue=[2, 3]>
[30] pry(#<Thread::Queue>):1>
また、前セクションに書いたように、binding.pry
を用いれば、実行中のメソッドのローカル変数を確認することもできます。
ドキュメントを読み書きする
作成したクラスにはドキュメントを付けておきます。ドキュメントは、所定の形式で記せばpryから閲覧することができますし、他のコマンドラインツールでHTMLなどに自動で変換することもできます。
まずは、先程作成したQueue
クラスをファイルに保存します。カレントディレクトリのqueue.rb
に保存するには、以下のようにします。
[30] pry(main)> save-file Queue --to './queue.rb'
queue.rb successfully saved
[31] pry(main)>
edit {ファイル名}
で、保存したソースコードを編集できます。今回は、RubyのメジャーなドキュメンテーションスタイルであるYARDに従ってドキュメントを書きます。
[31] pry(main)> edit './queue.rb'
# FIFOのデータ構造
class Queue
def initialize(initial_list)
@queue = initial_list
end
# @return [Array] 現在のキュー
attr_reader :queue
# キューに値を格納する
# @param value [*object] キューに格納するオブジェクト。
# @return [Array] 値を格納した後のキュー
def enqueue(value)
@queue.push(value)
end
# キューから値を取り出す
# @return [object | nil] 最も古い要素。要素が一つもない場合はnil。
def dequeue()
@queue.shift
end
end
保存してエディタを閉じると、pryのセッションに戻り、編集後のコードが自動で読み込まれます。
ドキュメントを閲覧するには、show-source {クラス/メソッド名} -d
を実行します。show-source
には$
というエイリアスもあります。
[32] pry(main)> $ Queue -d
From: queue.rb:1
Class name: Thread::Queue
Number of lines: 23
FIFOのデータ構造
# ソースコード。長いので略。
[33] pry(main)>
[33] pry(main)> $ Queue#queue -d
From: queue.rb:7:
Owner: Thread::Queue
Visibility: public
Signature: queue()
Number of lines: 3
return [Array] 現在のキュー
attr_reader :queue
[34] pry(main)>
[34] pry(main)> $ Queue#enqueue -d
From: queue.rb:10:
Owner: Thread::Queue
Visibility: public
Signature: enqueue(value)
Number of lines: 7
キューに値を格納する
param value [*object] キューに格納するオブジェクト。
return [Array] 値を格納した後のキュー
def enqueue(value)
@queue.push(value)
end
[35] pry(main)>
[35] pry(main)> $ Queue#dequeue -d
From: queue.rb:17:
Owner: Thread::Queue
Visibility: public
Signature: dequeue()
Number of lines: 6
キューから値を取り出す
return [object | nil] 最も古い要素。要素が一つもない場合はnil。
def dequeue()
@queue.shift
end
[36] pry(main)>
その他
binding.pry
で停止した後にステップ実行するにはpry-byebug
が、pryをRailsで使うにはpry-rails
があります。