Code & Clay – Notes to self. Mainly Ruby/Rails.

What does the then method do in Ruby?

Object#then yields the value of the object to a block and returns the resulting value of the block.

As with #tap, #then helps us negate the need for temporary variables:

> sum = [1,2,3].sum
=> 6

> square = sum ** 2
=> 36

> "The result is #{square}!"
=> "The result is 36!"

The above example becomes:

[1,2,3].sum.then { |obj| obj ** 2 }
           .then { |obj| "The result is #{obj}!" }

Continuing the #tap example, perhaps we want to give a new user a random name:

class Name
  def self.random
    %w{ Lavender Sage Thyme }.sample
  end
end

It’s a little convoluted perhaps, but we can create a new randomly named user in a single line.

Name.random.then { |random_name| User.new.tap { |user| user.name = random_name } }

Name.random.then yields the random name to a block wherein we create a new user. Following the example from the previous post, we then assign the newly instantiated name inside the #tap block.

What does the tap method do in Ruby?

Object#tap yields the object it is called upon to a block but the resulting value is the value of the object (not the value of the block). For example:

> 1.tap { |obj| puts obj * 2 }
2
=> 1

The value 1 is passed to the block where we output the result of obj * 2 (2). However, the value of the expression is the value of the object: 1.

Say we have a User class:

class User
  attr_accessor :name
end

To create a new user, we might do something like this:

def user
  new_user = User.new
  new_user.name = @name
  new_user
end

The return value of the above method is a new user object with its name assigned to the value of @name.

You can see we have to create the temporary variable new_user to hold the value of the new user in order for us to assign its name. We then return the value of the temporary variable.

#tap removes the need for the temporary variable.

def user
  User.new.tap { |obj| obj.name = @name }
end

In the above example, we call #tap on the newly instantiated user object. Inside the block, we assign a name to the new user. The resulting value is the newly instantiated user object with its name assigned the value of @name.

Monkey-patching locally

In Ruby, classes are open. They can be modified at any time. You can add new methods to existing classes and even re-define methods. Rubyists call this monkey-patching.

In the example below, I’ve added String#word_count to return the number of words in a string.

class String
  def word_count
    self.split.count
  end
end
"Don't put your blues where your shoes should be.".word_count # => 9

However, this change is global. What happens if someone else defines their own version of String#word_count based on different rules? Say, they might want to count the individual parts of a contraction, or ignore digits and any non-word characters.

Refinements allow us to monkey-patch classes and modules locally.

Here, I’ve created a new module and used Module#refine to add word_count to String.

module MyStringUtilities
  refine String do
    def word_count
      self.split.count
    end
  end
end

The refinement isn’t available in the global scope:

"Take my shoes off and throw them in the lake.".word_count # => NoMethodError: undefined method `word_count' for…

To activate the refinment, I need to use using:

using MyStringUtilities

"Take my shoes off and throw them in the lake.".word_count # => 10
"A pseudonym to fool him".word_count # => 5

If I activate the refinement within a class or module, the refinement is available until the end of the class or module definition.

Below, the refinement is not available at the top level because it is scoped to MyClass.

module MyStringUtilities
  refine String do
    def word_count
      self.split.count
    end
  end
end

class MyClass
  using MyStringUtilities

  def self.number_of_words_in(string)
    string.word_count
  end
end

"Out on the wily, windy moors".word_count #=> NoMethodError

MyClass.number_of_words_in("Out on the wily, windy moors") #=> 6

Audiobook streaming platform on AWS stack and Ruby on Rails

Piotr discusses how he implemented an audiobook streaming site in Rails, along with the challenges he faced.

Audiobooks tend to take up a lot of disk space so often streaming is the only practical method of delivery. Piotr writes about using AWS to convert MP3s and using Cloudfront to handle the streaming.

How to rename database columns in Rails

Often when I begin a project, I find myself rolling back migrations, dropping the database, renaming tables and columns whilst I figure out the shape of the model. If I need to rename a column, I’ll just update or delete the corresponding migration.

However, it is possible to change a column’s name in a migration.

Say, I have a table tasks and I want to change the name of the title column to name. First I ask Rails to generate the migration:

rails generate migration rename_title_to_name_in_tasks

The generated migration looks like this:

class RenameTitleToNameInTasks < ActiveRecord::Migration[6.0]
  def change
  end
end

I then remove change and add the up and down migration like so:

class RenameTitleToNameInTasks < ActiveRecord::Migration[6.0]
  def up
    rename_column :tasks, :title, :name
  end

  def down
    rename_column :tasks, :name, :title
  end
end

rename_column takes three arguments. The first is the table name. The second is the existing column name. The third is the new name.

Above, I have defined the up and down migration. That is, the up migration does the renaming bit when we migrate the database. The down migration runs when we roll back the migration – in this instance, it reverts the column’s name back to the original.

Before I run the migration, I give the schema a quick check.

  create_table "tasks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "title", null: false
    t.text "notes", default: ""
    t.uuid "task_list_id"
    t.uuid "fulfiller_id"
    t.datetime "completed_at", precision: 6
    t.index ["fulfiller_id"], name: "index_tasks_on_fulfiller_id"
    t.index ["task_list_id"], name: "index_tasks_on_task_list_id"
  end

Then, after running rails db:migrate, I check it again to see if the migration has worked:

  create_table "tasks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "name", null: false
    t.text "notes", default: ""
    t.uuid "task_list_id"
    t.uuid "fulfiller_id"
    t.datetime "completed_at", precision: 6
    t.index ["fulfiller_id"], name: "index_tasks_on_fulfiller_id"
    t.index ["task_list_id"], name: "index_tasks_on_task_list_id"
  end

All done!

Of course, five minutes later, I might decide that the new name probably isn’t the best and I can run rails db:rollback. On doing so, I can see that the column name has reverted to title.