A hands-on workshop exploring the dry-rb ecosystem - a collection of next-generation Ruby libraries that embrace functional programming concepts while remaining pragmatic and Ruby-like.
- Immutability by default - Data structures that don't change unexpectedly
- Explicit over implicit - Clear contracts and expectations
- Composition over inheritance - Build complex behavior from simple parts
- Type safety without ceremony - Catch errors early without boilerplate
| Part | Topic | Duration | Description |
|---|---|---|---|
| 1 | dry-types | 20 min | Type system for coercion, constraints, and composition |
| 2 | dry-struct | 15 min | Immutable, typed value objects |
| 3 | dry-validation | 25 min | Schemas and contracts for business rule validation |
| 4 | dry-monads | 25 min | Result, Maybe, and Try monads for error handling |
| 5 | dry-container | 15 min | IoC container and dependency injection |
| 6 | dry-transaction | 10 min | Business transaction DSL with failure handling |
# Clone the repository
git clone https://github.com/jwalsh/dry-rb-workshop.git
cd dry-rb-workshop
# Install dependencies
bundle install
# Run individual examples
ruby lib/examples/01_types_primitives.rb
ruby lib/examples/12_monads_result.rb
ruby lib/examples/19_transaction.rb
# Run the complete application demo
ruby lib/application.rbgem "dry-types", "~> 1.7"
gem "dry-struct", "~> 1.6"
gem "dry-validation", "~> 1.10"
gem "dry-monads", "~> 1.6"
gem "dry-container", "~> 0.11"
gem "dry-auto_inject", "~> 1.0"
gem "dry-transaction", "~> 0.15"# Strict types raise on invalid input
strict_string = Types::Strict::String
strict_string.("hello") # => "hello"
# Coercible types attempt conversion
coercible_int = Types::Coercible::Integer
coercible_int.("42") # => 42
# Constrained types with validation
PositiveInt = Types::Strict::Integer.constrained(gt: 0)
Email = Types::Strict::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+\.[a-z]+\z/i)class Person < Dry::Struct
attribute :name, Types::Strict::String
attribute :age, Types::Coercible::Integer
attribute :email, Types::Strict::String.optional
end
alice = Person.new(name: "Alice", age: "30", email: "alice@example.com")
older_alice = alice.new(age: 31) # Returns new instanceclass UserService
include Dry::Monads[:result, :do]
def create(params)
validated = yield validate(params)
user = yield persist(validated)
Success(user)
end
end
# Pattern matching on results
case service.create(params)
in Success(user) then puts "Created: #{user.name}"
in Failure(:validation, errors) then puts "Invalid: #{errors}"
in Failure(:database, error) then puts "DB Error: #{error}"
endclass CreateOrder
include Dry::Transaction
step :validate
step :check_inventory
step :charge_payment
step :create_record
step :send_confirmation
end| Type Family | Behavior | Example |
|---|---|---|
Types::Strict:: |
No coercion, raises on mismatch | Strict::String.("hi") |
Types::Coercible:: |
Ruby-style coercion | Coercible::Integer.("42") |
Types::Params:: |
HTTP param coercion | Params::Bool.("true") |
Types::JSON:: |
JSON-style coercion | JSON::Date.("2025-01-01") |
.constrained(gt: 0) # greater than
.constrained(gteq: 0) # greater than or equal
.constrained(min_size: 1) # minimum length
.constrained(format: /regex/) # regex match
.constrained(included_in: []) # enum values| Operation | Description |
|---|---|
Success(val) |
Wrap success value |
Failure(err) |
Wrap failure value |
.value! |
Unwrap (raises on Failure) |
.value_or(x) |
Unwrap with default |
.bind { } |
Chain (must return Result) |
.fmap { } |
Transform success value |
yield result |
Do-notation unwrap |
- dry-rb Documentation
- dry-types
- dry-struct
- dry-validation
- dry-monads
- dry-container
- dry-transaction
- ROM (Ruby Object Mapper) - uses dry-rb
- Hanami Framework - built on dry-rb
MIT