18
.
02
.
2024
18
.
02
.
2024
Ruby
Software

Lazy attributes in Ruby

Krzysztof Wawer
Ruby Developer
Lazy Attributes in Ruby - Krzysztof Wawer

What are lazy attributes?

Lazy attributes are properties or variables in a programming language that are not computed until their value is specifically requested or accessed. Instead of being calculated immediately when the object is created or when the attribute is defined, lazy attributes defer their computation until needed. This can be useful for optimizing performance, especially when dealing with large or complex data structures, by only computing values when required.

Do we have lazy attributes in Ruby?

Ruby doesn't have built-in lazy attributes like some other languages. However, you can achieve lazy initialization of attributes in Ruby using various techniques. We can see lazy attributes in dry-initializer, reform, … . Let’s focus right now on example with dry-initializer .

# game.rb

class Game
  extend Dry::Initializer

  option :board_id,    default: -> { server.fetch_board_id }
  option :duration,    default: -> { server.fetch_game_duration }
  option :max_players, default: -> { 4 }

  # ...
end

The syntax involving lambda might appear unconventional when compared to other Ruby examples. However, a significant advantage of using lambda or proc is that it allows us to receive a value after invoking call method. This capability proves especially beneficial when the default value is obtained from a database query.

To gain a better understanding of this feature, let's explore a simpler example involving hashes.

default_game_attributes = {
  max_players: -> { server.fetch_max_players }
  duration: -> { server.fetch_game_duration }
}

game_params = {
  board_id: 1,
  max_players: 2
}

game_attributes = {
  **default_game_attributes,
  **game_params
}

game_attributes
# => {
#   board_id: 1,
#   duration: #<Proc:0x000000010d6c3208@-e:1 (lambda)>
#   max_players: 2
# }

The code above optimizes by saving a database query for fetching the max_players value in the default_game_attributes hash. However, it introduces a side effect: the duration key now holds a lambda object instead of a number. This tradeoff needs careful consideration. Before using the game_attributes hash elsewhere, we must resolve every callable object. This can be achieved with a simple method.

def resolve_lazy_hash(hsh)
  hsh.deep_transform_values do |value|
    value.respond_to?(:call) ? value.call : value
  end
end

resolve_lazy_hash(game_attributes)
# => {
#   board_id: 1,
#   duration: 60
#   max_players: 2
# }

Back to the class

# game.rb

class Game
  extend Dry::Initializer

  option :board_id,    default: -> { server.fetch_board_id }
  option :duration,    default: -> { server.fetch_game_duration }
  option :max_players, default: -> { 4 }

  # ...
end

dry-initializer operates by requiring a proc (or any callable object) for default values, making it impossible to assign raw values directly. In the example above, one additional requirement is needed: Game class must have #server method. Let's implement it.

# game.rb

class Game
  extend Dry::Initializer

  option :board_id,    default: -> { server.fetch_board_id }
  option :duration,    default: -> { server.fetch_game_duration }
  option :max_players, default: -> { 4 }

  def server
    @server ||= Server.new
  end
end

# server.rb

class Server
  def logger
    logger ||= Logger.new(STDOUT)
  end

  def fetch_board_id
    logger.info("#fetch_board_id called")

    1
  end

  def fetch_game_duration
    logger.info("#fetch_game_duration called")

    60
  end
end

Once we have everything we need, we can run it.

game = Game.new(board_id: 123)
pp game

# ...]  INFO -- : #fetch_game_duration called
#<Game:0x000000010c27dfa0 @board_id=123, @duration=60, @max_players=4, @server=#<Game::Server:0x000000010c27db68>>

game = Game.new
pp game

# ...]  INFO -- : #fetch_board_id called
# ...]  INFO -- : #fetch_game_duration called
#<Game:0x0000000108a1efd8 @board_id=1, @duration=60, @max_players=4, @server=#<Game::Server:0x0000000108a1eb78>>

When are lazy attributes useful?

Hash and attributes are quite similar. Wrapping values with proc gives two features:

  • access to instance methods
  • postpone computations until we need the value
Krzysztof Wawer
Ruby Developer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

Vector Search in Ruby - Paweł Strzałkowski

Vector Search in Ruby

17
.
03
.
2024
Paweł Strzałkowski
ChatGPT
Embeddings
Postgresql
Ruby
Ruby on Rails
LLM Embeddings in Ruby - Paweł Strzałkowski

LLM Embeddings in Ruby

17
.
03
.
2024
Paweł Strzałkowski
Ruby
LLM
Embeddings
ChatGPT
Ollama
Handling Errors in Concurrent Ruby, Michał Łęcicki

Handling Errors in Concurrent Ruby

14
.
11
.
2023
Michał Łęcicki
Ruby
Ruby on Rails
Tutorial
Recap of Friendly.rb 2024 conference

Insights and Inspiration from Friendly.rb: A Ruby Conference Recap

02
.
10
.
2024
Kaja Witek
Conferences
Ruby on Rails

Covering indexes - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Postgresql
Backend
Ula Sołogub - SQL Injection in Ruby on Rails

The Deadly Sins in RoR security - SQL Injection

14
.
11
.
2023
Urszula Sołogub
Backend
Ruby on Rails
Software
Michal - Highlights from Ruby Unconf 2024

Highlights from Ruby Unconf 2024

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality
Cezary Kłos - Optimizing Cloud Infrastructure by $40 000 Annually

Optimizing Cloud Infrastructure by $40 000 Annually

14
.
11
.
2023
Cezary Kłos
Backend
Ruby on Rails

Smooth Concurrent Updates with Hotwire Stimulus

14
.
11
.
2023
Michał Łęcicki
Hotwire
Ruby on Rails
Software
Tutorial

Freelancers vs Software house

02
.
10
.
2024
Michał Krochecki
Visuality
Business

Table partitioning in Rails, part 2 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
Ruby on Rails

N+1 in Ruby on Rails

14
.
11
.
2023
Katarzyna Melon-Markowska
Ruby on Rails
Ruby
Backend

Turbo Streams and current user

29
.
11
.
2023
Mateusz Bilski
Hotwire
Ruby on Rails
Backend
Frontend

Showing progress of background jobs with Turbo

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Ruby
Hotwire
Frontend
Backend

Table partitioning in Rails, part 1 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend
Ruby on Rails

Table partitioning types - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend

Indexing partitioned table - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
SQL Views in Ruby on Rails

SQL views in Ruby on Rails

14
.
11
.
2023
Jan Grela
Backend
Ruby
Ruby on Rails
Postgresql
Design your bathroom in React

Design your bathroom in React

14
.
11
.
2023
Bartosz Bazański
Frontend
React
Lazy Attributes in Ruby - Krzysztof Wawer

Lazy attributes in Ruby

14
.
11
.
2023
Krzysztof Wawer
Ruby
Software

Exporting CSV files using COPY - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Ruby
Ruby on Rails
Michał Łęcicki - From Celluloid to Concurrent Ruby

From Celluloid to Concurrent Ruby: Practical Examples Of Multithreading Calls

14
.
11
.
2023
Michał Łęcicki
Backend
Ruby
Ruby on Rails
Software

Super Slide Me - Game Written in React

14
.
11
.
2023
Antoni Smoliński
Frontend
React