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

Rubyの強力なREPLである[pry](https://github.com/pry/pry/wiki)の基本的な使い方と、RubyのREPL駆動開発について記します。

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

1. プロジェクトのソースコードを編集する
2. 実行可能な段階になったら、開発環境にデプロイするなどして動作を確認する
3. 上手くいかないところを直す
4. 1.に戻る

ですが、2.の段階まで仕上げるのは結構時間がかかり、その間に間違いも犯しやすいです。一方、REPLを開発の中心に据えると、コードを手軽に実行でき、内部の状態も把握しやすいため、コーディングのフィードバックが非常に早く得られます。


## pryのインストール

pryをインストールするには、以下のコマンドを実行します。

```shell-session
$ gem install pry pry-doc
```

MacやLinuxで、ルート権限でない場合は、以下のコマンドを実行します。

```shell-session
$ sudo gem intall pry pry-doc
```

pryは本体。pry-docがあると、Ruby組み込みのクラスやメソッドのドキュメントやソースコードを見ることができます。

インストールできたらpryを起動します。

```shell-session
$ pry
[1] pry(main)>
```

少し触ってみると、まずirbとは異なり、シンタックスハイライトや、Tabによる入力補完が効くことが分かります。


## コードを書いてファイルに保存する

とりあえず、適当なコードを書いてみます。

```shell-session
[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`コマンドを使えば、指定したメソッドをエディタで編集できます。

```shell-session
[3] pry(main)>edit fizzbuzz
```

起動したエディタで、メソッドの内容を以下のように修正し、保存してエディタを閉じれば、pryのセッションに戻ります。

```ruby
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で実行してみます。

```shell-session
[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に変更したければ、以下のようにします。

```shell-session
$ echo "Pry.editor = 'vim'" >> ~/.pryrc
```

`'vim'`の部分は、`'emacs'`でも`'code'`でも、PATHの通ったディレクトリにある任意のエディタを指定して下さい。

プログラムが完成したので、このメソッドはファイルに保存します。ファイルに保存するには、`save-file {メソッド名} --to {ファイル名}`を実行します。たとえば、`fizzbuzz`メソッドをカレントディレクトリの`fizzbuzz.rb`に保存したければ、次のようにします。

```shell-session
[5] pry(main)>save-file fizzbuzz --to './fizzbuzz.rb'
```

新たにpryを起動する時に、既存のファイルを読み込む場合は、`-r`オプションを使います。たとえば、`fizzbuzz/rb`を読み込んでpryを起動したければ、以下のようにします。

```shell-session
$ pry -r "./fizzbuzz.rb"
```


## コードをデバッグする

続いて、もう少し複雑なプログラムを書いてみます。バブルソートを書くことにします。隣り合う要素を比較し、昇順になっていなければ順番を入れ替えるアルゴリズムです。

```shell-session
[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`を編集します。

```shell-session
[9] pry(main)>edit bubble_sort
```

エラーが発生する`i`の値を確かめるために、以下のコードを挿入します。

```ruby
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が起動します。どういうことかというと、こういうことです。

```shell-session
[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で評価することができます。

```shell-session
[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`を実行します。

```shell-session
[5] pry(main)> exit
```

元のコンテクストに戻ってきたら、`edit bubble_sort`でコードを修正します。

```ruby
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のセッションに戻り、修正を確認します。

```shell-session
[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)>
```

修正が確認できましたので、ソースコードを保存します。

```shell-session 
[15] pry(main)>save-file bubble_sort --to './bubble_sort.rb'
```

上の例では、変数`i`の値を参照しただけでしたが、もちろん通常REPLで行うのと同じように、ローカル変数に値を再代入してからコードを再実行することもできます。

```shell-session
[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つ作成します。

```shell-session
[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`に移動するには、以下のようにします。

```shell-session
[22] pry(main)> cd q1
[23] pry(#<Thread::Queue>):1>
```

`ls`で、現在のコンテクストで参照可能なオブジェクトを一覧できます。


```shell-session
[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`を実行します。

```shell-session
[25] pry(#<Thread::Queue>):1> exit
=> #<Thread::Queue:0x00007fa72c80ca48 @queue=[1]>
```

同様に、`q2`のコンテクストも覗いてみると、インスタンス変数`@queue`の値が異なることが確認できます。

```shell-session
[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`に保存するには、以下のようにします。

```shell-session
[30] pry(main)> save-file Queue --to './queue.rb'
queue.rb successfully saved
[31] pry(main)>
```

`edit {ファイル名}`で、保存したソースコードを編集できます。今回は、Rubyのメジャーなドキュメンテーションスタイルである[YARD](https://rubydoc.info/gems/yard/file/docs/GettingStarted.md)に従ってドキュメントを書きます。

```shell-session
[31] pry(main)> edit './queue.rb'
```

```ruby: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`には`$`というエイリアスもあります。

```shell-session
[32] pry(main)> $ Queue -d

From: queue.rb:1
Class name: Thread::Queue
Number of lines: 23

FIFOのデータ構造

# ソースコード。長いので略。
[33] pry(main)>
````

```shell-session
[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)>
```

```shell-session
[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)> 
```

```shell-session
[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`があります。