Functional Ruby Programming with Trailblazer

Embracing Functional Concepts in an Object-Oriented World

2018-11-29-functional-programming-with-trailblazer-green-hamster-in-headphones.png

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!