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

Hashes with default procs

Our app uses a list of bird names.

bird_names = %w(
  redbreast
  crow
  eagle
  hedge-sparrow
  sparrow
  pigeon
  owl
  penguin
)

One day, someone mentions that a couple of the birds – the redbreast and hedge-sparrow – are now more commonly known as the robin and dunnock. Not wanting to confuse our users, we decide we should use the modern vernacular.

Instead of going back and changing the data we can create a new list of bird names – updating any names that have changed with their modern equivalents.

We pair the old with the new names in a hash.

bird_names_and_modern_equivalents = {
  "redbreast" => "robin",
  "hedge-sparrow" => "dunnock"
}

As in the previous post, we treat the hash as a proc.

> bird_names.map &bird_names_and_modern_equivalents

The result isn’t quite what we want. We have the new names for the redbreast and the dunnock but all the other birds are now nil.

=> ["robin", nil, nil, "dunnock", nil, nil, nil, nil]

We want our list to contain the original names if there are no substitions in our hash.

Just as we can set a default hash value, we can also set a default proc.

Below, if we ask the hash for the value of a key that doesn’t exist, it returns the value of the default proc instead. In this case, it returns the name of the bird we asked for unchanged.

bird_names_and_modern_equivalents.default_proc = Proc.new { |_h, k| k }
> bird_names.map &bird_names_and_modern_equivalents
=> ["robin", "crow", "eagle", "dunnock", "sparrow", "pigeon", "owl", "penguin"]

Hash#to_proc

In Ruby, we can convert hashes to procs.

> {}.respond_to?(:to_proc)
=> true

> {}.to_proc
=> #<Proc:0x000000010741f7a0 (lambda)>

We could do something like this.

my_hash = {
  1 => "thumb",
  2 => "shoe",
  3 => "knee"
}

> [*1..3].map(&my_hash)
=> ["thumb", "shoe", "knee"]

The hash is called with each element of the array.

If an element in the array does not correspond with a key in the hash, it maps to nil.

> [*1..4].map(&my_hash)
=> ["thumb", "shoe", "knee", nil]

Hash defaults

You can use a pair of empty braces to initialise a hash. But, if you try to access a non-existant key, Ruby returns nil by default.

my_hash = {}
=> {}
my_hash[:foo]
=> nil

Sometimes it might be useful to set a default value. This could be anything: an empty string, a null object, an error…

Here, I’ve set a string as default.

my_hash = {}
=> {}
my_hash.default = "Not set"
=> "Not set"
my_hash[:foo]
=> "Not set"
my_hash[:bar]
=> "Not set"

Now, instead of getting nil for a non-existant entry, I get the default value.

The hash remains empty because I have not created any entries.

my_hash
=> {}

Hash.new accepts a proc which you can use to set default values.

my_hash = Hash.new { "Not set" }
=> {}
my_hash[:foo]
=> "Not set"
my_hash
=> {}

You can also tell the proc to create a new entry if one isn’t found.

my_hash = Hash.new { |hash, key| hash[key] = "Not set" }
=> {}
my_hash[:foo]
=> "Not set"
my_hash[:bar]
=> "Not set"
my_hash
=> {:foo=>"Not set", :bar=>"Not set"}

See the ruby docs for more info.

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. It is an alias of Object#yield_self.

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.

Use inverse_of when creating associations with non-standard naming

I have two models ‘Customer’ and ‘GreenPencil’.

A customer can have many green pencils. But, as far as the customer is concerned, these are just pencils. It makes sense for me to call the relationship that.

I first establish that a green pencil belongs to a customer.

class GreenPencil < ApplicationRecord
  belongs_to :customer, required: true
  #...

Now, when I establish the other side of the relationship (remember, each GreenPencil has a foreign key pointing to its Customer) I can do this:

class Customer < ApplicationRecord
  has_many :pencils,
           class_name: 'GreenPencil',
           inverse_of: :customer

I’m telling Rails that a Customer has many pencils, the associated class is GreenPencil and that this relationship is the other side of GreenPencil’s relationship with a customer that I established at the start.

Rails usually sets inverse_of on all associations itself. However, because the class name differs from the association name, we need to specifiy it explicitly.

Rails will infer the inverse relationship if we set foreign_key but I think inverse_of is clearer to the reader.

Nested attributes

Sometimes it’s desirable to update one model’s attributes through an associated parent. Say, I might provide a form that asks a user for their name and email address but the user’s name is stored in the users table and their email address is recorded in another.

Rails provides the accepts_nested_attributes_for macro.

class User < ApplicationRecord
    has_one :contact_information, dependent: :destroy, required: true
    accepts_nested_attributes_for :contact_information

This allows me to update a user’s contact information like so:

user.update_attributes( contact_information_attributes: {
    phone_number: '123',
    email_address: 'hey@you.com
})

Instead of the headache of multiple forms and multiple controllers, I can now nest the contact information fields within the user form.

<%= bootstrap_form_for(@user) do |f| %>
  <%= f.text_field :name, required: true, autocomplete: "name" %>
  # ...
  <%= f.fields_for :contact_information do |ff| %>
      <%= ff.text_field :email_address, %>
      # ...

Finally, I need to specify the associated model’s attributes as allowable parameters in the parent resource’s controller:

def strong_params
    params.require(:user).permit(
      :name,
      contact_information_attributes: %i[
        email_address
      ]
    )
end