You might be surprised to learn that you’re already using many functional programming (FP) approaches in your Ruby code without realizing it. While languages like Haskell enforce these principles, Ruby and Python leave it up to the programmer to make conscious choices. Let’s explore how we can leverage these FP concepts in Ruby, particularly when using the Trailblazer (TRB) framework.
Key Functional Programming Concepts in Ruby and Trailblazer
1. Embrace Immutability
In Trailblazer, we treat incoming data as immutable. The only mutable data structure is the internal hash (options[:my_key]
or ctx[:my_other_key]
in TRB 2.1). When transforming data, always assign the result to a new key instead of modifying in place:
def transform_data(ctx, input_array:, **)
ctx[:transformed_array] = input_array.map { |item| item.upcase }
end
Consider using gems like Hamster for immutable data structures, or simply maintain self-discipline in your approach.
2. Simplify Function Signatures
Keep your method signatures simple, limiting them to 2-4 attributes. This practice enhances testability and reduces code complexity. For most Trailblazer steps, this structure suffices:
def my_step(ctx, params:, **)
# Your logic here
end
Take advantage of named parameters and default values when applicable.
3. Break Down Complex Operations
Divide your methods, steps, and operations into smaller, more manageable units. This approach may require more initial setup but pays dividends in maintainability and composability. Treat your Trailblazer operations as functional objects:
class CreateUser < Trailblazer::Operation
step :validate
step :persist
step :send_welcome_email
# Each step is a small, focused function
end
4. Eliminate External State
Ensure your operations receive everything they need when called, avoiding reliance on global or external state. This doesn’t preclude the use of global configs, but their values should be provided explicitly:
result = CreateUser.(params: user_params, config: AppConfig)
5. Leverage Trailblazer’s Functional Design
Trailblazer is built with functional concepts in mind. Many steps can be implemented as lambda functions, embracing a pure functional approach:
class UpdateProfile < Trailblazer::Operation
step ->(ctx, params:, **) { params[:name].present? }
# More steps...
end
6. Consider Adding Type Checking
While Ruby is dynamically typed, you can add type checking to your codebase using gems like Sorbet or RBS. This can help catch errors early and improve code clarity:
# Using Sorbet
require 'sorbet-runtime'
class User
extend T::Sig
sig { params(name: String, age: Integer).void }
def initialize(name, age)
@name = name
@age = age
end
end
7. Implement Lazy Evaluation
For large data structures, consider using lazy evaluation to improve performance:
large_collection = (1..1_000_000).lazy.map { |n| n * 2 }.select(&:even?)
Many of these functional programming principles are likely already part of your Ruby toolkit, even if you haven’t explicitly labeled them as such. By consciously applying these concepts, especially when working with Trailblazer, you can write more maintainable, testable, and robust code.
Remember, while Ruby may not be Haskell, it offers the flexibility to incorporate functional programming paradigms alongside its object-oriented nature. Embrace this hybrid approach to level up your Ruby and Trailblazer projects!