LoginSignup
1
0

More than 3 years have passed since last update.

【Ruby】pryによるREPL駆動開発

Last updated at Posted at 2020-12-15

Rubyの強力なREPLであるpryの基本的な使い方と、RubyのREPL駆動開発について記します。

REPL駆動開発とは、REPLを中心とする開発スタイルのことです。REPL駆動でない一般的な開発フローは、

  1. プロジェクトのソースコードを編集する
  2. 実行可能な段階になったら、開発環境にデプロイするなどして動作を確認する
  3. 上手くいかないところを直す
  4. 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になってしまったということです。つまり、numsi番目と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'
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があります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0