San Francisco, USA

5214F Diamond Heights Blvd #553
San Francisco, CA 94131

Pune, India

203, Jewel Towers, 2nd Floor
Lane Number 5, Koregaon Park
Pune 411001, India

301 - 275 - 3997
hello@BigBinary.com

Rails 6.1 deprecates rails db:structure:dump and rails db:structure:load

This blog is part of our Rails 6.1 series.

Rails 6.1 deprecates rails db:structure:load and rails db:structure:dump tasks.

Before Rails 6.1, executing rake db:schema:dump would dump db/schema.rb file. And executing rake db:structure:dump would dump db/structure.sql file.

Rails provides config.active_record.schema_format setting for which the valid values are :ruby or :sql. However, since there are specific tasks for db:structure and db:schema this value was not really being used.

Changes in Rails 6.1

In Rails 6.1 the Rails team decided to combine the two different tasks into a single task. In Rails 6.1 rails db:structure:dump and rails db:structure:load have been deprecated and the following message would be shown.

Using `bin/rails db:structure:dump` is deprecated and will be removed in Rails 6.2. Configure the format using `config.active_record.schema_format = :sql` to use `structure.sql` and run `bin/rails db:schema:dump` instead.

Now Rails will start taking into the account value set for config.active_record.schema_format.

rails db:schema:dump and rails db:schema:load would do the right thing based on the value set for config.active_record.schema_format.

Check out the pull request for more details on this.


Ruby 2.8 adds endless method definition

This blog is part of our Ruby 2.8 series.

Ruby 2.8 adds endless method definition. It enables us to create method definitions without the need of end keyword. It is marked as an experimental feature.

# endless method definition
>> def raise_to_power(number, power) = number ** power

>> raise_to_power(2, 5)

=> 32

The discussion around it can be found here. Check out the pull request for more details on this.


Rails 6.1 adds --minimal option support

This blog is part of our Rails 6.1 series.

rails new my_app creates a new Rails application fully loaded with all the features.

If we want to omit some of the features then we needed to skip them like this.

# before Rails 6.1

$ rails new tiny_app
    --skip-action-cable
    --skip-action-mailer
    --skip-action-mailbox
    --skip-action-text
    --skip-active-storage
    --skip-bootsnap
    --skip-javascript
    --skip-spring
    --skip-system-test
    --skip-webpack-install
    --skip-turbolinks

Before Rails 6.1 it was not possible to skip things like active_job and jbuilder.

Rails 6.1

Rails 6.1 added a new option --minimal.

$ rails new tiny_app --minimal

All the following are excluded from this minimal Rails application.

  • action_cable
  • action_mailbox
  • action_mailer
  • action_text
  • active_job
  • active_storage
  • bootsnap
  • jbuilder
  • spring
  • system_tests
  • turbolinks
  • webpack

We can bundle webpack in this minimal app like this.

$ rails new tiny_app --minimal webpack=react

Database option can also be passed.

$ rails new tiny_app --minimal --database postgresql webpack=react

Check out the pull request for more details on this.


Rails 6 adds support to persist timezones of Active Job

This blog is part of our Rails 6 series. Rails 6.0 was recently released.

When a job is enqueued in Rails 6 using Active Job, the current timezone of a job is preserved and then this preserved timezone is restored when the job is finished executing.

Let’s take an example of sale at Amazon.

Amazon would like to remind users across different timezones about its upcoming sale by sending an email. This task of sending a reminder would be processed as a background job.

Before:

Before Rails 6, we had to pass timezone explicitly to the perform method of the job as shown below.

timezone = "Eastern Time (US & Canada)"

AmazonSaleJob.perform_later(Time.now, timezone)

class AmazonSaleJob < ApplicationJob
  queue_as :default

  def perform(time, timezone)

    time = time.in_time_zone(timezone)
    sale_start_time = localtime(2020, 12, 24)

    if time >= sale_start_time
      puts "Sale has started!"
      #Send an email stating Sale has started
    else
      sale_starts_in = (sale_start_time - time).div(3600)
      puts "Hang on! Sale will start in #{sale_starts_in} hours"
      #Send an email stating sales starts in sale_starts_in hours
     end
  end

  private

    def localtime(*args)
      Time.zone ? Time.zone.local(*args) : Time.utc(*args)
    end
end

After:

After the changes in Rails 6, passing timezone to Job is now taken care of by Rails.

timezone = "Eastern Time (US & Canada)"

Time.use_zone(timezone) do
  AmazonSaleJob.perform_later(Time.zone.now)
end

class AmazonSaleJob < ApplicationJob
  queue_as :default

  def perform(time)
    sale_start_time = localtime(2020, 12, 24)

    if time >= sale_start_time
      puts "Sale has started!"
      #Send an email stating Sale has started
    else
      sale_starts_in = (sale_start_time - time).div(3600)
      puts "Hang on! Sale will start in #{sale_starts_in} hours"
      #Send an email stating sales starts in sale_starts_in hours
     end
   end

  private

    def localtime(*args)
      Time.zone ? Time.zone.local(*args) : Time.utc(*args)
    end
end

Rails 6 also propagates timezone to all the subsequent nested jobs.


Ruby 2.7 adds Beginless Range

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Ruby 2.7 added support for Beginless Range which makes the start of range an optional parameter.

(..100) is a Beginless Range and it is equivalent to (nil..100).

Let’s see how Beginless Range could be used.

> array = (1..10).to_a

# Select first 6 elements
> array[..5]
=> [1, 2, 3, 4, 5, 6]

# Select first 5 elements
> array[...5]
=> [1, 2, 3, 4, 5]

# grep (INFINITY..5) in (1..5)
> (1..10).grep(..5)
=> [1, 2, 3, 4, 5]

# (..100) is equivalent to (nil..100)
> (..100) == (nil..100)
=> true

Here is another example where in the case statement the condition can be read as below the specified level.

case temperature
when ..-15
  puts "Deep Freeze"
when -15..8
  puts "Refrigerator"
when 8..15
  puts "Cold"
when 15..25
  puts "Room Temperature"
when (25..)   # Kindly notice the brackets here
  puts "Hot"
end

It can also be used for defining constants for ranges.

TEMPERATURE = {
  ..-15  => :deep_freeze,
  -15..8 => :refrigerator,
  8..15  => :cold,
  15..25 => :room_temperature,
  25..   => :hot
end

Using Beginless Range in DSL makes it easier to write conditions and it looks more natural.

# In Rails
User.where(created_at: (..DateTime.now))
# User Load (2.2ms)  SELECT "users".* FROM "users" WHERE "users"."created_at" <= $1 LIMIT $2  [["created_at", "2020-08-05 15:00:19.111217"], ["LIMIT", 11]]


# In RubySpec
ruby_version(..'1.9') do
# Tests for old Ruby
end

Here is the relevant commit and discussion regarding this change.


Rails 6 adds Array#including/excluding and Enumerable#including/excluding

This blog is part of our Rails 6 series. Rails 6.0 was recently released.

Rails 6 added including and excluding on Array and Enumerable.

Array#including and Enumerable#including

including can be used to extend a collection in a more object oriented way. It does not mutate the original collection but returns a new collection which is the the concatenation of the given collections.

# multiple arguments can be passed to including
>> [1, 2, 3].including(4, 5)
=> [1, 2, 3, 4, 5]

# another enumerable can also be passed to including
>> [1, 2, 3].including([4, 5])
=> [1, 2, 3, 4, 5]

>> %i(apple orange).including(:banana)
=> [:apple, :orange, :banana]

# return customers whose country_code is IN along with the prime customers
>> Customer.where(country_code: "IN").including(Customer.where(prime: true))

Array#excluding and Enumerable#excluding

It returns a copy of the enumerable excluding the given collection.

# multiple arguments can be passed to including
>> [11, 22, 33, 44].excluding([22, 33])
=> [11, 44]

>> %i(ant bat cat).excluding(:bat)
=> [:ant, :cat]

# return all prime customers except those who haven't added their phone
>> Customer.where(prime: true).excluding(Customer.where(phone: nil))

Array#excluding and Enumerable#excluding replaces the already existing method without which in Rails 6 is now aliased to excluding.

>> [11, 22, 33, 44].without([22, 33])
=> [11, 44]

excluding and including helps to shrink or extend a collection without using any operator.

Check out the commit for more details on this.


Rails 6.1 raises on db:rollback for multiple database applications

This blog is part of our Rails 6.1 series.

Rails 6.1 adds support to handle db:rollback in case of multiple database application.

Prior to this change, on executing db:rollback Rails used to rollback the latest migration from the primary database. If we passed on a [:NAME] option along with to specify the database, we used to get an error. Check out the issue for more details.

Rails 6.0.0

> rails db:rollback:secondary

rails aborted!
Don't know how to build task `db:rollback:secondary` (See the list of available tasks with `rails --tasks`)
Did you mean?  db:rollback

Staring with Rails 6.1, we need to pass the database name along with db:rollback:[NAME] otherwise a RuntimeError is raised.

Rails 6.1.0

> rails db:rollback

rails aborted!
You're using a multiple database application. To use `db:migrate:rollback` you must run the namespaced task with a VERSION. Available tasks are db:migrate:rollback:primary and db:migrate:rollback:secondary.

> rails db:rollback:primary

== 20200731130500 CreateTeams: reverting ======================================
-- drop_table(:teams)
   -> 0.0060s
== 20200731130500 CreateTeams: reverted (0.0104s) =============================

Check out the pull request for more details on this.


How to render liquid templates when the template refers to other liquid templates

Shopify’s Liquid Templates is a great way for templating in Ruby on Rails applications.

If the template is as simple as this one then there are no issues.

{% if user %}
  Hello {{ user.name }}
{% endif %}

However sometimes we have a liquid template which is using another liquid template. Here is an example.

home.liquid
<!DOCTYPE html>
<html>
  <head>
    <style>{% asset 'main.css' %}</style>
  </head>
  <body>
    {% partial 'header' %}
    <h1>Home Page</h1>
  </body>
</html>

In the above case home.liquid is using two other liquid templates main.css and header.liquid.

Let’ see what these templates look like.

main.css
* {
  color: {{ theme.text_color }};
}
a {
  color: {{ theme.link_color }};
}
header.liquid
<nav>
{{ organization.name }}
</nav>

In order to include the assets and the partials we need to create liquid tags.

Let’s create a tag which will handle assets.

# app/lib/liquid/tags/asset.rb

module Liquid
  module Tags
    class Asset < Liquid::Tag
      def initialize(tag_name, name, tokens)
        super
        @name = name.strip.remove("'")
      end

      def render(context)
        new_context = context.environments.first
        asset = Template.asset.find_by(filename: @name)

        Liquid::Template.parse(asset.content).render(new_context).html_safe
      end
    end
  end
end

Let’s create a tag that will handle partials.

# app/lib/liquid/tags/partial.rb

module Liquid
  module Tags
    class Partial < Liquid::Tag
      def initialize(tag_name, name, tokens)
        super
        @name = name.strip.remove("'")
      end

      def render(context)
        new_context = context.environments.first

        # Remember here we are not passing extension
        asset = Template.partial.find_by(filename: @name + ".liquid")

        Liquid::Template.parse(asset.content).render(new_context).html_safe
      end
    end
  end
end

Let’s create a new initializer and we need to register these tags in that initializer.

# config/initializers/liquid.rb

require 'liquid/tags/asset'
require 'liquid/tags/partial'

Liquid::Template.register_tag('asset', Liquid::Tags::Asset)
Liquid::Template.register_tag('partial', Liquid::Tags::Partial)

Restart the server and now we can render the home.liquid template like this.

template = Template.template.find_by(filename: "home.liquid")

attributes = {
  organization: {
    name: "Example"
  },
  theme: {
    text_color: "#000000",
    link_color: "#DBDBDB"
  }
}

Liquid::Template.parse(template.content).render(attributes).html_safe

Here we have a simple implementation of the tags. We can do much more, if needed, like looping over items to parse each item from the partial. That can be done by registering a separate tag for the item and passing in the id of the item so that the specific item can be found and parsed.


Rails 6.1 deprecates the use of return, break or throw to exit a transaction block

This blog is part of our Rails 6.1 series.

Rails 6.1 deprecates the use of return, break or throw to exit a transaction block.

return / break

>> Post.transaction do
>>   @post.update(post_params)
>>
>>   break # or return
>> end

# => TRANSACTION (0.1ms)  begin transaction
# => DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is
# => deprecated without replacement. If the `throw` came from
# => `Timeout.timeout(duration)`, pass an exception class as a second
# => argument so it doesn't use `throw` to abort its block. This results
# => in the transaction being committed, but in the next release of Rails
# => it will rollback.
# => TRANSACTION (0.8ms)  commit transaction

throw

>> Timeout.timeout(1) do
>>   Post.transaction do
>>     @post.update(post_params)
>>
>>     sleep 3 # simulate slow request
>>   end
>> end

# => TRANSACTION (0.1ms)  begin transaction
# => DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is
# => deprecated without replacement. If the `throw` came from
# => `Timeout.timeout(duration)`, pass an exception class as a second
# => argument so it doesn't use `throw` to abort its block. This results
# => in the transaction being committed, but in the next release of Rails
# => it will rollback.
# => TRANSACTION (1.6ms)  commit transaction
# => Completed 500 Internal Server Error in 1022ms (ActiveRecord: 3.2ms | Allocations: 9736)
# => Timeout::Error (execution expired)

Here, even when the error was thrown the transaction is committed. This is something which is going to change in the future versions.

This is done because currently, when a transaction block is wrapped in Timeout.timeout(duration) i.e. without second argument(an exception class) then it uses throw to exit the transaction.

Solution

>> Timeout.timeout(1, Timeout::Error) do
>>   Post.transaction do
>>     @post.update(post_params)
>>
>>     sleep 3 # simulate slow request
>>   end
>> end

# => TRANSACTION (0.1ms)  begin transaction
# => TRANSACTION (0.7ms)  rollback transaction
# => Timeout::Error (execution expired)

Check out the pull request for more details on this.


Rails 6.1 automatically generates an abstract class when using multiple databases

This blog is part of our Rails 6.1 series.

Rails started supporting multiple databases from Rails 6.0. To use a specific database, we can specify the database connection in the model using connects_to. In the following case we want Person model to connect to crm database.

class Person < ApplicationRecord
  connects_to database: { writing: :crm }
end

As the application grows, more and more models start sharing the same database. Now a lot of models may contain connects_to call to the same database.

class Person < ApplicationRecord
  connects_to database: { writing: :crm }
end

class Order < ApplicationRecord
  connects_to database: { writing: :crm }
end

class Sale < ApplicationRecord
  connects_to database: { writing: :crm }
end

In order to avoid the duplication, we can create an abstract class connecting to a database and manually inherit all other models from that class. This could look like this.

class CrmRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :crm }
end

class Person < CrmRecord
end

class Order < CrmRecord
end

class Sale < CrmRecord
end

Rails 6.1

Before Rails 6.1 we had no choice but to create that abstract class manually. Rails 6.1 allows us to generate an abstract class when we are generating a model using scaffold.

$ rails g scaffold Person name:string --database=crm

It creates an abstract class with the database’s name appended with Record. The generated model automatically inherits from the new abstract class.

# app/models/users_record.rb
class CrmRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :crm }
end

# app/models/admin.rb
class Person < CrmRecord
end

If the abstract class already exists, it is not created again. We can also use an existing class as the abstract class by passing parent option to the scaffold command.

$ rails g scaffold Customer name:string --database=crm --parent=PrimaryRecord

This skips generating CrmRecord class as we have specified Rails to use PrimaryRecord abstract class as its parent.

Check out the pull request for more details on this.


Rails 6.1 adds annotate_rendered_view_with_filenames to annotate HTML output

This blog is part of our Rails 6.1 series.

Rails 6.1 makes it easier to debug rendered HTML by adding the name of each template used.

Rails 6.1

Add the following line in development.rb file to enable this feature.

config.action_view.annotate_rendered_view_with_filenames = true

Now the rendered HTML will contain comment indicating the beginning and end of each template.

Here is an example.

Annotated HTML output

In the image we can see the begin and end for each of the templates. It helps a lot in debugging webpages to find out which template is rendered. Check out the pull request for more details on this.


Rails 6.1 allows enums attributes to configure the default value

Rails 6.1 makes it easier to configure a default value for Active Record enum attributes.

Let’s take an example of blog posts with status and category columns.

class Post < ApplicationRecord
  enum status: %i[draft reviewed published]
  enum category: { rails: "Rails", react: "React" }
end

Before Rails 6.1, defaults for enum attributes can be configured by applying default on the database level.

class AddColumnStatusToPosts < ActiveRecord::Migration[6.0]
  def change
    add_column :posts, :status, :integer, default: 0
    add_column :posts, :category, :string, default: "Rails"
  end
end

After Rails 6.1, defaults for enum attributes can be configured directly in the Post model using _default option.

class Post < ApplicationRecord
  enum status: %i[draft reviewed published], _default: "draft"
  enum category: { rails: "Rails", react: "React" }, _default: "Rails"
end

The new approach to set enum defaults has following advantages. Let’s understand keeping the context of Post model with category as an example.

  • When the category default value changes from Rails to React. We have to add a new migration in Rails 6 and previous versions to update the database column default.
  • Let say the default value for post category(i.e: Rails) is removed from the enum from Post model. Rails 6 and previous versions wouldn’t throw an exception and continue to work without setting any default value. Rails 6.1 with new syntax would raise an exception.

Check out the pull request for more details on this.


Rails 6.1 adds support for where with a comparison operator

Rails 6.1 adds support to comparison operator in the where clause. The four comparison operators supported are:

  • Greater than (>).
  • Greater than equal to (>=).
  • Less than (<).
  • Less than equal to (<=).

The comparison operator is also supported by the finder methods in ActiveRecord which internally uses where clause, for example: find_by, destroy_by, delete_by.

The new style for comparisons has to follow advantages:

  • The where clause with the comparison operator doesn’t raise an exception when ActiveRecord::Relation uses ambiguous column name.
  • The where clause with the comparison operator handle proper precision of the database columns.

Before Rails 6.1, to add a condition with comparison in where clause, we had to add raw SQL notation.

Rails 6.0.0

>> Post.where("DATE(published_at) > DATE(?)", Date.today)
# => <ActiveRecord::Relation [...]>

>> Post.find_by("likes < ?", 10)
# => <ActiveRecord::Relation [...]>

# Following query on execution would raise exception.
>> Post.joins(:comments).where("likes > 10")
# => ambiguous column name: id

Rails 6.1.0

>> Post.where("published_at >": Date.today)
# => <ActiveRecord::Relation [...]>

>> Post.find_by("likes <": 10)
# => <ActiveRecord::Relation [...]>

# Following query on execution would NOT raise exception.
>> Post.joins(:comments).where("likes >": 10)
# => <ActiveRecord::Relation [...]>

Check out the pull request for more details on this.


Rails 6.1 tracks Active Storage variant in the database

This blog is part of our Rails 6.1 series.

Active Storage variants are the transformation of the original image. These variants can be used as thumbnails, avatars, etc.

Active Storage generates variants on demand by downloading the original image. The image is transformed into a variant and is stored to the third party services like S3.

When a request to fetch a variant for an Active Storage object is made, Rails checks if the variant is already been processed and is already available on S3 or not. But to do so Rails has to make a call to find out if the variant is available on S3. This extra call adds to the latency.

Active Storage has to wait until the image variant check call is completed because S3 might not return the image when a GET request is made due to eventual consistency. This way Rails avoid downloading a broken image from S3 and uploading broken image variant to S3 in case the variant is not present.

In Rails 6.1, Active Storage tracks the presence of the variant in the database. This change avoids unnecessary variant presence remote request made to the S3 and directly fetches or generates a image variant.

In Rails 6.1, the configuration to allow variant tracking in the database is by default set to true.

config.active_storage.track_variants: true

Check out the pull request for more details on this.


Ruby 2.7 adds Enumerable#filter_map

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Ruby 2.7 adds Enumerable#filter_map which is a combination of filter + map as the name indicates. The ‘filter_map’ method filters and map the enumerable elements within a single iteration.

Before Ruby 2.7, we could have achieved the same with 2 iterations using select & map combination or map & compact combination.

irb> numbers = [3, 6, 7, 9, 5, 4]

# we can use select & map to find square of odd numbers

irb> numbers.select { |x| x.odd? }.map { |x| x**2 }
=> [9, 49, 81, 25]

# or we can use map & compact to find square of odd numbers

irb> numbers.map { |x| x**2 if x.odd? }.compact
=> [9, 49, 81, 25]

Ruby 2.7

Ruby 2.7 adds Enumerable#filter_map which can be used to filter & map the elements in a single iteration and which is more faster when compared to other options described above.

irb> numbers = [3, 6, 7, 9, 5, 4]
irb> numbers.filter_map { |x| x**2 if x.odd? }
=> [9, 49, 81, 25]

The original discussion had started 8 years back. Here is the latest thread and github commit for reference.


Ruby 2.7 deprecates conversion of keyword arguments

A notable change has been announced for Ruby 3 for which deprecation warning has been added in Ruby 2.7. Ruby 2.7 deprecated automatic conversion of keyword arguments and positional arguments. This conversion will be completely removed in Ruby 3.

Ruby 2.7 NEWS has listed the spec of keyword arguments for Ruby 3.0. We will take the examples mentioned there and for each scenario we will look into how we can fix them in the existing codebase.

Scenario 1

When method definition accepts keyword arguments as the last argument.

def sum(a: 0, b: 0)
  a + b
end

Passing exact keyword arguments in a method call is acceptable, but in applications we usually pass a hash to a method call.

sum(a: 2, b: 4) # OK

sum({ a: 2, b: 4 }) # Warned

In this case, we can add a double splat operator to the hash to avoid deprecation warning.

sum(**{ a: 2, b: 4 }) # OK

Scenario 2

When method call passes keyword arguments but does not pass enough required positional arguments.

If the number of positional arguments doesn’t match with method definition, then keyword arguments passed in method call will be considered as the last positional argument to the method.

def sum(num, x: 0)
  num.values.sum + x
end
sum(a: 2, b: 4) # Warned

sum(a: 2, b: 4, x: 6) # Warned

To avoid deprecation warning and for code to be compatible with Ruby 3, we should pass hash instead of keyword arguments in method call.

sum({ a: 2, b: 4 }) # OK

sum({ a: 2, b: 4}, x: 6) # OK

Scenario 3

When a method accepts a hash and keyword arguments but method call passes only hash or keyword arguments.

If a method arguments are a mix of symbol keys and non-symbol keys, and the method definition accepts either one of them then Ruby splits the keyword arguments but also raises a warning.

def sum(num={}, x: 0)
  num.values.sum + x
end
sum("x" => 2, x: 4) # Warned

sum(x: 2, "x" => 4) # Warned

To fix this warning, we should pass hash separately as defined in the method definition.

sum({ "x" => 4 }, x: 2) # OK

Scenario 4

When an empty hash with double splat operator is passed to a method that doesn’t accept keyword arguments.

Passing keyword arguments using double splat operator to a method that doesn’t accept keyword argument will send empty hash similar to earlier version of Ruby but will raise a warning.

def sum(num)
  num.values.sum
end
numbers = {}
sum(**numbers) # Warned

To avoid this warning, we should change method call to pass hash instead of using double splat operator.

numbers = {}
sum(numbers) # OK

Added support for non-symbol keys

In Ruby 2.6.0, support for non-symbol keys in method call was removed. It is added back in Ruby 2.7. When method accepts arbitrary keyword arguments using double splat operator then non-symbol keys can also be passed.

def sum(**num)
  num.values.sum
end
ruby 2.6.5
sum("x" => 4, "y" => 3)
=> ArgumentError (wrong number of arguments (given 1, expected 0))

sum(x: 4, y: 3)
=> 7
ruby 2.7.0
sum("x" => 4, "y" => 3)
=> 7

sum(x: 4, y: 3)
=> 7

Added support for **nil

Ruby 2.7 added support for **nil to explicitly mention if a method doesn’t accept any keyword arguments in method call.

def sum(a, b, **nil)
  a + b
end

sum(2, 3, x: 4)
=> ArgumentError (no keywords accepted)

To suppress above deprecation warnings we can use -W:no-deprecated option.

In conclusion, Ruby 2.7 has worked big steps towards changing specification of keyword arguments which will be completely changed in Ruby 3.

For more information on discussion, code changes and official documentation, please head to Feature #14183 discussion, pull request and NEWS release.


Ruby 2.7 adds Enumerator::Lazy#eager

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Ruby 2.0 introduced Enumerator::Lazy, a special type of enumerator which helps us in processing chains of operations on a collection without actually executing it instantly.

By applying Enumerable#lazy method on any enumerable object, we can convert that object into Enumerator::Lazy object. The chains of actions on this lazy enumerator will be evaluated only when it is needed. It helps us in processing operations on large collections, files and infinite sequences seamlessly.

# This line of code will hang and you will have to quit the console by Ctrl+C.
irb> list = (1..Float::INFINITY).select { |i| i%3 == 0 }.reject(&:even?)

# Just adding `lazy`, the above line of code now executes properly
# and returns result without going to infinite loop. Here the chains of
# operations are performed as and when it is needed.
irb> lazy_list = (1..Float::INFINITY).lazy.select { |i| i%3 == 0 }.reject(&:even?)
=> #<Enumerator::Lazy: ...>

irb> lazy_list.first(5)
=> [3, 9, 15, 21, 27]

When we chain more operations on Enumerable#lazy object, it again returns lazy object without executing it. So, when we pass lazy objects to any method which expects a normal enumerable object as an argument, we have to force evaluation on lazy object by calling to_a method or it’s alias force.

# Define a lazy enumerator object.
irb> list = (1..30).lazy.select { |i| i%3 == 0 }.reject(&:even?)
=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>

# The chains of operations will return again a lazy enumerator.
irb> result = list.select { |x| x if x <= 15 }
=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>:map>

# It returns error when we call usual array methods on result.
irb> result.sample
irb> NoMethodError (undefined method `sample'
irb> for #<Enumerator::Lazy:0x00007faab182a5d8>)

irb> result.length
irb> NoMethodError (undefined method `length'
irb> for #<Enumerator::Lazy:0x00007faab182a5d8>)

# We can call the normal array methods on lazy object after forcing
# its actual execution with methods as mentioned above.
irb> result.force.sample
=> 9

irb> result.to_a.length
=> 3

The Enumerable#eager method returns a normal enumerator from a lazy enumerator, so that lazy enumerator object can be passed to any methods which expects a normal enumerable object as an argument. Also, we can call other usual array methods on the collection to get desired results.

# By adding eager on lazy object, the chains of operations would return
# actual result here. If lazy object is passed to any method, the
# processed result will be received as an argument.
irb> eager_list = (1..30).lazy.select { |i| i%3 == 0 }.reject(&:even?).eager
=> #<Enumerator: #<Enumerator::Lazy: ... 1..30>:select>:reject>:each>

irb> result = eager_list.select { |x| x if x <= 15 }
irb> result.sample
=> 9

irb> result.length
=> 3

The same way, we can use eager method when we pass lazy enumerator as an argument to any method which expects a normal enumerator.

irb> list = (1..10).lazy.select { |i| i%3 == 0 }.reject(&:even?)
irb> def display(enum)
irb>   enum.map { |x| p x }
irb> end

irb>  display(list)
=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>:map>

irb> eager_list = (1..10).lazy.select { |i| i%3 == 0 }.reject(&:even?).eager
irb> display(eager_list)
=> 3
=> 9

Here’s the relevant commit and feature discussion for this change.


Ruby 2.7 introduces numbered parameters as default block parameters

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

At some point, all of us have used names like a, n, i etc for block parameters. Below are few examples where numbered parameters can come in handy.

> (1..10).each { |n| p n * 3 }

> { a: [1, 2, 3], b: [2, 4, 6], c: [3, 6, 9] }.each { |_k, v| p v }

> [10, 100, 1000].each_with_index { |n, i| p n, i }

Ruby 2.7 introduces a new way to access block parameters. Ruby 2.7 onwards, if block parameters are obvious and we wish to not use absurd names like n or i etc, we can use numbered parameters which are available inside a block by default.

We can use _1 for first parameter, _2 for second parameter and so on.

Here’s how Ruby 2.7 provides numbered parameters inside a block. Below shown are the examples from above, only this time using numbered parameters.

> (1..10).each { p _1 * 3 }

> { a: [1, 2, 3], b: [2, 4, 6], c: [3, 6, 9] }.each { p _2 }

> [10, 100, 1000].each_with_index { p _1, _2 }

Like mentioned in News-2.7.0 docs, Ruby now raises a warning if we try to define local variable in the format _1. Local variable will have precedence over numbered parameter inside the block.

> _1 = 0
> => warning: `_1' is reserved for numbered parameter; consider another name

> [10].each { p _1 }
> => 0

Numbered parameters are not accessible inside the block if we define ordinary parameters. If we try to access _1 when ordinary parameters are defined, then ruby raises SyntaxError like shown below.

> ["a", "b", "c"].each_with_index { |alphabet, index| p _1, _2}

=> SyntaxError ((irb):1: ordinary parameter is defined)

This feature was suggested 9 years back and came back in discussion last year. After many suggestions community agreed to use _1 syntax.

Head to following links to read the discussion behind numbered parameters, Feature #4475 and Discussion #15723.

Here’s relevant commit for this feature.


Rails 6 fixes a bug where after_commit callbacks are called on failed update in a transaction block

This blog is part of our Rails 6 series. Rails 6.0 was recently released.

Rails 6 fixes a bug where after_commit callbacks are called on failed update in a transaction block.

Let’s checkout the bug in Rails 5.2 and the fix in Rails 6.

Rails 5.2

Let’s define an after_commit callback in User model and try updating an invalid user object in a transaction block.

>> class User < ApplicationRecord
>>   validates :name, :email, presence: true
>>
>>   after_commit :show_success_message
>>
>>   private
>>
>>     def show_success_message
>>       p 'User has been successfully saved into the database.'
>>     end
>> end

=> :show_success_message

>> user = User.create(name: 'Jon Snow', email: 'jon@bigbinary.com')
begin transaction
User Create (0.8ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Jon Snow"], ["email", "jon@bigbinary.com"], ["created_at", "2019-07-14 15:35:33.517694"], ["updated_at", "2019-07-14 15:35:33.517694"]]
commit transaction
"User has been successfully saved into the database."

=> #<User id: 1, name: "Jon Snow", email: "jon@bigbinary.com", created_at: "2019-07-14 15:35:33", updated_at: "2019-07-14 15:35:33">

>> User.transaction do
>>   user.email = nil
>>   p user.valid?
>>   user.save
>> end
begin transaction
false
commit transaction
"User has been successfully saved into the database."

=> false

As we can see here, that that the after_commit callback show_success_message was called even if object was never saved in the transaction.

Rails 6.0.0.rc1

Now, let’s try the same thing in Rails 6.

>> class User < ApplicationRecord
>>   validates :name, :email, presence: true
>>
>>   after_commit :show_success_message
>>
>>   private
>>
>>     def show_success_message
>>       p 'User has been successfully saved into the database.'
>>     end
>> end

=> :show_success_message

>> user = User.create(name: 'Jon Snow', email: 'jon@bigbinary.com')
SELECT sqlite_version(*)
begin transaction
User Create (1.0ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Jon Snow"], ["email", "jon@bigbinary.com"], ["created_at", "2019-07-14 15:40:54.022045"], ["updated_at", "2019-07-14 15:40:54.022045"]]
commit transaction
"User has been successfully saved into the database."

=> #<User id: 1, name: "Jon Snow", email: "jon@bigbinary.com", created_at: "2019-07-14 15:40:54", updated_at: "2019-07-14 15:40:54">

>> User.transaction do
>>   user.email = nil
>>   p user.valid?
>>   user.save
>>   end
false

=> false

Now, we can see that after_commit callback was never called if the object was not saved.

Here is the relevant issue and the pull request.


Ruby 2.7 adds Enumerable#tally

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Let’s say that we have to find the frequency of each element of an array.

Before Ruby 2.7, we could have achieved it using group_by or inject.

irb> scores = [100, 35, 70, 100, 70, 30, 35, 100, 45, 30]

# we can use group_by to group the scores

irb> scores.group_by { |v| v }.map { |k, v| [k, v.size] }.to_h
=> {100=>3, 35=>2, 70=>2, 30=>2, 45=>1}

# or we can use inject to group the scores

irb> scores.inject(Hash.new(0)) {|hash, score| hash[score] += 1; hash }
=> {100=>3, 35=>2, 70=>2, 30=>2, 45=>1}

Ruby 2.7

Ruby 2.7 adds Enumerable#tally which can be used to find the frequency. Tally makes the code more readable and intuitive. It returns a hash where keys are the unique elements and values are its corresponding frequency.

irb> scores = [100, 35, 70, 100, 70, 30, 35, 100, 45, 30]
irb> scores.tally
=> {100=>3, 35=>2, 70=>2, 30=>2, 45=>1}

Check out the github commit for more details on this.