[Ruby] REPL-driven development with pry

This section describes the basic usage of pry, which is a powerful REPL of Ruby, and REPL-driven development of Ruby.

REPL-driven development is a development style centered around REPL. The general development flow that is not REPL driven is

  1. Edit the project source code
  2. When it is ready to be executed, check the operation by deploying it in the development environment.
  3. Fix things that don't work
  4. Return to 1.

However, it takes a lot of time to finish up to stage 2, and it is easy to make mistakes during that time. On the other hand, putting the REPL at the center of development makes it easier to execute code and understand the internal state, so you can get coding feedback very quickly.

pry installation

To install pry, run the following command.

$ gem install pry pry-doc

On Mac or Linux, if you do not have root privileges, execute the following command.

$ sudo gem intall pry pry-doc

pry is the main body. With pry-doc, you can see the documentation and source code for Ruby built-in classes and methods.

After installation, start pry.

$ pry
[1] pry(main)>

If you touch it a little, you can see that syntax highlighting and input completion by Tab are effective, unlike irb.

Write code and save to file

For the time being, I will write an appropriate code.

[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)> 

This code converts multiples of 3 to Fizz, multiples of 5 to Buzz, and multiples of 15 to FizzBuzz, and outputs an integer from 1 to 15. But something is wrong. 15 is now Fizz instead of FizzBuzz. Since 15 is also a multiple of 3, the cause is that the first condition n% 3 == 0 was matched before the third condition n% 15 == 0. ..

You have to rewrite the fizzbuzz method. However, typing 13 lines again is a hassle. You can use the edit command to edit the specified method in the editor.

[3] pry(main)>edit fizzbuzz

In the editor you started, modify the contents of the method as follows, save and close the editor, and you will be returned to the pry session.

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

Try running the modified fizzbuzz method in the REPL.

[4] pry(main)> fizzbuzz(15)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
=> 1

This time it went well.

By the way, I think that the editor launched by the edit command is nano on Mac and Linux and notepad on Windows by default, but you can change it with the pry configuration file. The pry configuration file is ~/.pryrc and the editor property is Pry.editor. So, for example, if you want to change the editor to vim:

$ echo "Pry.editor = 'vim'" >> ~/.pryrc

For the 'vim' part, specify any editor in the directory in your PATH, whether it is'emacs' or 'code'.

Now that the program is complete, save this method to a file. To save to a file, run save-file {method name} --to {filename}. For example, if you want to save the fizzbuzz method to fizzbuzz.rb in the current directory:

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

If you want to read an existing file when you start a new pry, use the -r option. For example, if you want to load fizzbuzz/rb and start pry, do the following:

$ pry -r "./fizzbuzz.rb"

Debug code

Next, let's write a slightly more complicated program. I will write a bubble sort. It is an algorithm that compares adjacent elements and changes the order if they are not in ascending order.

[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)> 

I got an error. It is said that Integer and nil cannot be compared. The exception is thrown on line 5, so if i is any value,num [i]ornum [i + 1]will be nil, and let's compare them. Is the cause. Find out the cause of the error and fix it. Edit bubble_sort in the editor.

[9] pry(main)>edit bubble_sort

Insert the following code to see the value of i where the error occurs.

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? # <=Inserted code
      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

When binding.pry is evaluated by the Ruby processing system, pry will be started in that context. This is what it means.

[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)> 

Time has stopped when the 5th line of bubble_sort is executed. In this state, variables in this scope can be evaluated by REPL as follows.

[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)> 

By examining the variable just before the error, I found that when i is 2, i + 1 points outside the boundaries of the array, so nums [i + 1] is nil. It means that it has become. In other words, because it refers to the ith and i + 1th of nums," i + 1 is from 1 to right_end, not " i is from 0 to right_end". That is, i must move in the range from 0 to right_end --1. Therefore, fix it that way.

To rerun the code, run exit.

[5] pry(main)> exit

When you're back in the original context, fix the code with 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 to right_end -To 1
      # binding.Remove 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

Save your edits and return to your pry session to see your corrections.

[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)>

Now that you have confirmed the fix, save the source code.

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

In the above example, we just referenced the value of the variable i, but of course you can reassign the value to a local variable and then re-execute the code, just as you would normally do with a 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)> 

As you can see, using pry makes it very quick to validate and modify your code.

Move the context

A context is information about which object or method you are currently inside. As you move the context, you can reference or modify the instance variables of that object or the local variables of the method.

As an example, create a class Queue that represents the data structure of the FIFO and two instances of it.

[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)>

To move the context, run cd {destination object}. For example, to move to the q1 created earlier, do the following:

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

You can use ls to list the objects that can be referenced in the current context.

[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>

To exit the current context and return to the original context, run exit.

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

Similarly, if you look in the context of q2, you can see that the value of the instance variable @ queue is different.

[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> 

You can also use binding.pry to see the local variables of the method being executed, as described in the previous section.

Read and write documents

Attach a document to the created class. The document can be viewed from pry if it is written in the specified format, or it can be automatically converted to HTML etc. with other command line tools.

First, save the Queue class you created earlier to a file. To save to queue.rb in the current directory:

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

You can edit the saved source code with edit {filename}. This time, I will write the document according to YARD, which is a major documentation style of Ruby.

[31] pry(main)> edit './queue.rb'

queue.rb


#FIFO data structure
class Queue
  def initialize(initial_list)
    @queue = initial_list
  end

  # @return [Array]Current queue
  attr_reader :queue

  #Store the value in the queue
  # @param value [*object]Objects to queue.
  # @return [Array]Queue after storing values
  def enqueue(value)
    @queue.push(value)
  end

  #Retrieving a value from the queue
  # @return [object | nil]The oldest element. Nil if there are no elements.
  def dequeue()
    @queue.shift
  end
end

After saving and closing the editor, you will be returned to the pry session and the edited code will be loaded automatically.

To view the documentation, run show-source {class/method name} -d. show-source also has an alias of$.

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

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

FIFO data structure

#Source code. Omitted because it is long.
[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]Current queue

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

Store the value in the queue
param value [*object]Objects to queue.
return [Array]Queue after storing values

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

Retrieving a value from the queue
return [object | nil]The oldest element. Nil if there are no elements.

def dequeue()
  @queue.shift
end
[36] pry(main)>

Other

There is pry-byebug to step after stopping at binding.pry, and pry-rails to use pry with Rails.

Recommended Posts

[Ruby] REPL-driven development with pry
Install Ruby 3.0.0 with asdf
Plugin development with ImageJ
[Rails] Development with MySQL
11th, Classification with Ruby
Evolve Eevee with Ruby
Ruby version switching with rbenv
Solve Google problems with Ruby
I tried DI with Ruby
GraphQL Client starting with Ruby
Leap year judgment with Ruby
Format Ruby with VS Code
Integer check method with ruby
Ruby Learning # 8 Working With String
[Ruby] problem with if statement
Studying with CodeWar (ruby) ⑤ proc
Getting Started with Ruby Modules
[ruby] Method call with argument
Ruby on Rails development environment construction with Docker + VSCode (Remote Container)
How to install Pry after building Rails development environment with Docker
Steps to build a Ruby on Rails development environment with Vagrant
Ruby Scraping-Move Selenium Headless with VPS.
Learning Ruby with AtCoder 6 [Contest 168 Therefore]
Prepare Java development environment with Atom
Let's use Amazon Textract with Ruby
Programming with ruby (on the way)
Studying with CodeWar (ruby) ④ case ~ when
Ruby Study Memo (Test Driven Development)
Problems with android studio development series
Handle DatePicker with Ruby / gtk3 + glade3
Build jooby development environment with Eclipse
Impressions of making BlackJack-cli with Ruby
Html5 development with Java using TeaVM
Install ruby on Ubuntu 20.04 with rbenv
Make a typing game with ruby
How to build a Ruby on Rails development environment with Docker (Rails 6.x)
How to build a Ruby on Rails development environment with Docker (Rails 5.x)
Template: Build a Ruby / Rails development environment with a Docker container (Ubuntu version)
Template: Build a Ruby / Rails development environment with a Docker container (Mac version)