BigBinary Blog

We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.

Ruby 3.1 accumulates Enumerable#tally results

This blog is part of our Ruby 3.1 series.

We already know the Enumerable#tally method is used to count the occurrences of each element in an Enumerable collection. The #tally method was introduced in ruby 2.7.0. Please check our blog for more details on it.

Ruby 3.1 introduces an optional hash argument for the Enumerable#tally method to count. If a hash is given, the total number of occurrences of each element is added to the hash values and the final hash is returned.

Ruby 2.7.0+

1=> letters = ["a", "b", "c", "a", "d", "c", "a", "c", "a"]
2=> result = letters.tally
3=> {"a"=>4, "b"=>1, "c"=>3, "d"=>1}

Before Ruby 3.1

1=> new_letters = ["a", "b", "c", "a", "c", "a"]
2=> new_letters.tally(result)
3=> ArgumentError (wrong number of arguments (given 1, expected 0))

After Ruby 3.1

1=> new_letters = ["a", "b", "c", "a", "c", "a"]
2=> new_letters.tally(result)
3=> {"a"=>7, "b"=>2, "c"=>5, "d"=>1}

The value corresponding to each element in the hash must be an integer. Otherwise, the method raises TypeError on execution.

If the default value is defined for the given hash, it will be ignored and the count of occurrences will be added in the returned hash.

1=> letters = ["a", "b", "c", "a"]
2=> letters.tally(Hash.new(10))
3=> {"a"=>2, "b"=>1, "c"=>1}

Here's the relevant pull request and feature discussion for this change.

Ashik Salman in Ruby 3.1
April 20, 2021
Share

Rails 6.1 adds support for validating numeric values that fall within a specific range using the `in:` option

This blog is part of our Rails 6.1 series.

Before Rails 6.1, to validate a numerical value that falls within a specific range, we had to use greater_than_or_equal_to: and less_than_or_equal_to:.

In the example below, we want to add a validation that ensures that each item in the StockItem class has a quantity that ranges from 50 to 100.

1class StockItem < ApplicationRecord
2  validates :quantity, numericality: { greater_than_or_equal_to: 50, less_than_or_equal_to: 100 }
3end
4
5StockItem.create! code: 'Shirt-07', quantity: 40
6#=> ActiveRecord::RecordInvalid (Validation failed: Quantity must be greater than or equal to 50)

In Rails 6.1, to validate that a numerical value falls within a specific range, we can use the new in: option:

1class StockItem < ApplicationRecord
2  validates :quantity, numericality: { in: 50..100 }
3end
4
5StockItem.create! code: 'Shirt-07', quantity: 40
6#=> ActiveRecord::RecordInvalid (Validation failed: Quantity must be in 50..100)

Check out the pull request for more details on this feature.

Akanksha Jain in Rails, Rails 6.1
April 14, 2021
Share

Ruby 3.1 adds Enumerable#compact and Enumerator::Lazy#compact

This blog is part of our Ruby 3.1 series.

We are familiar with the compact method associated with arrays. The compact method returns a copy of the array after removing all nil elements.

Ruby 3.1 introduces the compact method in the Enumerable module. Now we can use the compact method along with the Enumerator and Enumerator::Lazy classes which include the Enumerable module.

Before Ruby 3.1

1=> enum = [1, nil, 3, nil, 5].to_enum
2=> #<Enumerator: ...>
3
4=> enum.compact
5=> NoMethodError (undefined method `compact' for #<Enumerator: [1, nil, 3, nil, 5]:each>)
6
7=>  enum.reject { |x| x.nil? }
8=> [1, 3, 5]

After Ruby 3.1

1=> enum = [1, nil, 3, nil, 5].to_enum
2=> #<Enumerator: ...>
3
4=> enum.compact
5=> [1, 3, 5]

We can access the compact method to remove all nil occurrences from any classes where we include the Enumerable module.

1class Person
2  include Enumerable
3
4  attr_accessor :names
5
6  def initialize(names = [])
7    @names = names
8  end
9
10  def each &block
11    @names.each(&block)
12  end
13end
14
15=> list = Person.new(["John", nil, "James", nil])
16=> #<Person:0x0000000101cd3de8 @names=["John", nil, "James", nil]>
17
18=> list.compact
19=> ["John", "James"]

Similarly, lazy evaluation can be chained with the compact method to remove all nil entries from the Enumerator collection.

1=> enum = [1, nil, 3, nil, 5].to_enum.lazy.compact
2=> #<Enumerator::Lazy: ...>
3
4=> enum.force
5=> [1, 3, 5]
6
7
8=> list = Person.new(["John", nil, "James", nil]).lazy.compact
9=> #<Enumerator::Lazy: ...>
10
11=> list.force
12=> ["John", "James"]

Here's the relevant pull request and feature discussion for this change.

Ashik Salman in Ruby 3.1
April 6, 2021
Share

Rails 7 allows constructors (build_association and create_association) on has_one :through associations

This blog is part of our Rails 7 series.

What are build_association and create_association constructors?

When we declare either a belongs_to or has_one association, the declaring class automatically gains the following methods related to the association:

  1. build_association(attributes = {})
  2. create_association(attributes = {})

In the above methods _association is replaced with the symbol(association name) passed as the first argument while declaring the associations. For example:

1class Book < ApplicationRecord
2  belongs_to :author
3end
4
5@book.build_author(name: 'John Doe', email: 'john_doe@example.com')
6#=> Returns a new Author object, instantiated with the passed attributes
7#=> Links through the book's object foreign key
8#=> New author object won't be saved in the database
9
10@book.create_author(name: 'John Doe', email: 'john_doe@example.com')
11#=> Returns a new Author object, instantiated with the passed attributes
12#=> Links through the book's object foreign key
13#=> The new author object will be saved in the database after passing all of the validations specified on the Author model

Before Rails 7

The build_association and create_association constructors were only supported by belongs_to and has_one associations.

Consider the example below. We have a model, Member, that has a has_one association with the CurrentMembership model. It also has a has_one :through association with the Club model.

1class Member < ApplicationRecord
2  has_one :current_membership
3  has_one :club, through: :current_membership
4end
5
6@member.build_club
7#=> NoMethodError (undefined method `build_club' for #<Member:0x00007f9ea2ebd3e8>)
8#=> Did you mean?  build_current_membership
9
10@member.create_club
11#=> NoMethodError (undefined method `create_club' for #<Member:0x00007f9ea2ebd3e8>)
12#=> Did you mean?  create_current_membership

After Rails 7

Users are allowed to use constructors (build_association and create_association) on has_one :through associations along with belongs_to and has_one associations.

1class Member < ApplicationRecord
2  has_one :current_membership
3  has_one :club, through: :current_membership
4end
5
6@member.build_club
7#=> #<Club:0x00007f9ea01a8ce0>
8
9@member.create_club
10#=> #<Club:0x00007f9ea01a8ce0>

Check out this pull request for more details.

Akanksha Jain in Rails, Rails 7
April 6, 2021
Share

Rails 6.1 adds delegated_type to ActiveRecord

This blog is part of our Rails 6.1 series.

Rails 6.1 adds delegated_type to ActiveRecord which makes it easier for models to share responsibilities.

Before Rails 6.1

Let's say we are building software to manage the inventory of an automobile company. It produces 2 types of vehicles, Car and Motorcycle. Both have name and mileage attributes.

Let's look into at least 2 different solutions to design this system.

Single Table Inheritance

In this approach, we combine all the attributes of various models and store them in a single table. Let's create a Vehicle model and its corresponding table to store the data of both Car and Motorcycle.

1# schema of Vehicle {id: Integer, type: String[car or motorcycle], name: String, mileage: Integer}
2class Vehicle < ApplicationRecord
3  # put common logic here
4end
5
6class Car < vehicle
7  # put car specific code & validation
8end
9
10class Motorcycle < Vehicle
11  # put motorcycle-specific code & validation
12end

This approach fits precisely for this scenario but when the attributes of the various models differ, it becomes a pain point. Let's say at some point in time we add a bs4_engine boolean column to track whether a Motorcyle has a bs4_engine or not. In the case of Car, bs4_engine will contain nil. As time passes, a lot of vehicle-specific attributes get added and the database will be sparsely filled with a lot of nil.

Polymorphic Relations

With polymorphic associations, a model can belong to more than one other model, on a single association.

1# schema {name: String, mileage: Integer}
2class Vehicle < ApplicationRecord
3  belongs_to :vehicleable, polymorphic: true
4end
5
6# schema {interior_color: String, adjustable_roof: Boolean}
7class Car < ApplicationRecord
8  has_one :vehicle, as: :vehicleable
9end
10
11# schema {bs4_engine: Boolean, tank_color: String}
12class Motorcycle < ApplicationRecord
13  has_one :vehicle, as: :vehicleable
14end

Here, Vehicle is a class that contains common attributes, while Motorcycle and Car store any diverging attributes. This approach fixes the nil values, but to create a Vehicle record we now have to create a Car or Motorcycle first separately.

1# creating new records
2>> bike = Motorcycle.create!( bs4_engine: false, tank_color: '#f2f2f2')
3#<Motorcycle id: 1, bs4_engine: true, tank_color: '#f2f2f2', created_at: "2021-01-17 ...">
4
5>> vehicle = Vehicle.create!(vehicleable: bike, name: 'TS-1987', mileage: 45)
6#<Vehicle id: 1, vehicleable_type: "Motorcycle", vehicleable_id: 1, name: "TS-1987", mileage: 45, created_at: ...">
7
8# query
9>> b1 = Motorcycle.find(1) #=> <Motorcycle id: 1, bs4_engine: true, tank_color: '#f2f2f2', created_at: "2021-01-17 ...">
10>> b1.vehicle.name  #=> TS-1987
11>> b1.vehicle.mileage  #=> 45

Now, let's say, we need to query Vehicles that are Motorcycles, or let's say we want to check whether a Vehicle is a Car or not. For all of these, we will have to write cumbersome logic and queries.

Rails 6.1 delegated_type

Rails 6.1 brings delegated_type which fixes the problem discussed above and adds a lot of helper methods. To use it, we just need to replace polymorphic relation with delegated_types.

1class Vehicle < ApplicationRecord
2  delegated_type :vehicleable, types: %w[ Motorcycle Car ]
3end

That is the only change we need to make to leverage the delegated_type. With this change, we can create both the delegator and delegatee at the same time.

1# creating new records
2>> vehicle1 = Vehicle.create!(vehicleable: Car.new(interior_color: '#fff', adjustable_roof: true), name: 'TS78Z', mileage: 89)
3#<Vehicle id: 3, vehicleable_type: "Car", vehicleable_id: 2, name: "TS78Z", mileage: 89, created_at: ...">
4
5>> vehicle2= Vehicle.create!(vehicleable: Motorcycle.new(bs4_engine: false, tank_color: '#ff00bb'), name: 'BL96', mileage: 45)
6#<Vehicle id: 4, vehicleable_type: "Motorcycle", vehicleable_id: 5, name: "BL96", mileage: 45, created_at: ...">
7
8# Note: Just initializing the delegatee(Car.new/Motorcycle.new) is sufficient.

When it comes to query capabilities, it adds a lot of delegated type convenience methods.

1# Get all Vehicles that are Cars
2>> Vehicle.cars
3#<ActiveRecord::Relation [#<Vehicle id: 5, vehicleable_type: "Car", vehicleable_id: 1, name: "TS78Z", ...">]>
4
5# Get all Vehicles that are Motorcycles
6>> Vehicle.motorcycles
7#<ActiveRecord::Relation [#<Vehicle id: 1, vehicleable_type: "Motorcycle", vehicleable_id: 1, name: "BL96", ...">]>
8
9
10>> vehicle = Vehicle.find(3)
11#<Vehicle id: 3, vehicleable_type: "Car", vehicleable_id: 2, name: "TS78Z", mileage: 89, created_at: ...">
12
13# check whether a Vehicle is a Car or Motorcycle
14>> vehicle.car?  #=> true
15>> vehicle.motorcylce? #=> false
16
17# get vehicleable
18>> vehicle.car # <Car id: 1, adjustable_roof: true, ...>
19>> vehicle.motorcycle # nil

So, delegated_type can be thought of as sugar on top of polymorphic relations that adds convenience methods.

Check out the pull request to learn more.

Akhil Gautam in Rails, Rails 6.1
April 6, 2021
Share
Older
Newer

Subscribe to our newsletter