Ruby 5.0: What If Ruby Had First-Class Types?

The article envisions a reimagined Ruby with optional, inline type annotations called TypedRuby, addressing limitations of current solutions like Sorbet and RBS. It proposes a syntax that integrates seamlessly with Ruby’s philosophy, emphasizing readability and gradual typing while considering generics and union types. TypedRuby represents a potential evolution in Ruby’s design.

After imagining a typed CoffeeScript, I realized we need to go deeper. CoffeeScript was inspired by Ruby, but what about Ruby itself? Ruby has always been beautifully expressive, but it’s also been dynamically typed from day one. And while Sorbet and RBS have tried to add types, they feel bolted on. Awkward. Not quite Ruby.

What if Ruby had been designed with types from the beginning? Not as an afterthought, not as a separate file you maintain, but as a natural, optional part of the language itself? Let’s explore what that could look like.

The Problem with Sorbet and RBS

Before we reimagine Ruby with types, let’s acknowledge why the current solutions haven’t caught on widely.

Sorbet requires you to add # typed: true comments and use a separate type checker. Types look like this:

# typed: true
extend T::Sig

sig { params(name: String, age: Integer).returns(String) }
def greet(name, age)
  "Hello #{name}, you are #{age}"
end
Code language: PHP (php)

RBS requires separate .rbs files with type signatures:

# user.rbs
class User
  attr_reader name: String
  attr_reader age: Integer
  
  def initialize: (name: String, age: Integer) -> void
  def greet: () -> String
end
Code language: CSS (css)

Both solutions have the same fundamental problem: they don’t feel like Ruby. Sorbet’s sig blocks are verbose and repetitive. RBS splits your code across multiple files, breaking the single-file mental model that makes Ruby so pleasant.

What we need is something that feels native. Something Matz might have designed if static typing had been a priority in 1995.

Core Design Principles

Let’s establish what TypedRuby should be:

  1. Types are optional everywhere. You can gradually type your codebase.
  2. Types are inline. No separate files, no sig blocks.
  3. Types feel like Ruby. Natural syntax that matches Ruby’s philosophy.
  4. Duck typing coexists with static typing. You choose when to be strict.
  5. Generic types are first-class. Collections, custom classes, everything.
  6. The syntax is minimal. Ruby is beautiful; types shouldn’t ruin that.

Basic Type Annotations

In TypeScript, you use colons. In Sorbet, you use sig blocks. TypedRuby could use a more natural Ruby approach with the :: operator we already know:

# Current Ruby
name = "Ivan"
age = 30

# TypedRuby with inline types
name :: String = "Ivan"
age :: Integer = 30

# Or with type inference
name = "Ivan"  # inferred as String
age = 30       # inferred as Integer
Code language: PHP (php)

The :: operator already means “scope resolution” in Ruby, but in this context (before assignment), it means “has type”. It’s familiar to Ruby developers and reads naturally.

Method Signatures

Current Sorbet approach:

extend T::Sig

sig { params(name: String, age: T.nilable(Integer)).returns(String) }
def greet(name, age = nil)
  age ? "Hello #{name}, #{age}" : "Hello #{name}"
end
Code language: JavaScript (javascript)

TypedRuby approach:

def greet(name :: String, age :: Integer? = nil) :: String
  age ? "Hello #{name}, #{age}" : "Hello #{name}"
end
Code language: JavaScript (javascript)

Or with Ruby 3’s endless method syntax:

def greet(name :: String, age :: Integer? = nil) :: String =
  age ? "Hello #{name}, #{age}" : "Hello #{name}"
Code language: JavaScript (javascript)

Much cleaner. The types are right there with the parameters, and the return type is at the end where it reads naturally: “define greet with these parameters, returning a String.”

Classes and Attributes

Current approach with Sorbet:

class User
  extend T::Sig
  
  sig { returns(String) }
  attr_reader :name
  
  sig { returns(Integer) }
  attr_reader :age
  
  sig { params(name: String, age: Integer).void }
  def initialize(name, age)
    @name = name
    @age = age
  end
end

TypedRuby approach:

class User
  attr_reader of String, :name
  attr_reader of Integer, :age
  
  def initialize(@name :: String, @age :: Integer)
  end
  
  def birthday :: void
    @age += 1
  end
  
  def greet :: String
    "I'm #{@name}, #{@age} years old"
  end
end
Code language: CSS (css)

Even better, we could introduce parameter properties like TypeScript:

class User
  def initialize(@name :: String, @age :: Integer, @email :: String)
    # @name, @age, and @email are automatically instance variables
  end
end
Code language: CSS (css)

Generics: The Ruby Way

This is where it gets interesting. Ruby already has a beautiful way of working with collections. TypedRuby needs to extend that naturally.

TypeScript uses angle brackets:

class Container<T> {
  private value: T;
  constructor(value: T) { this.value = value; }
}
Code language: JavaScript (javascript)

Sorbet uses square brackets:

class Container
  extend T::Generic
  T = type_member
  
  sig { params(value: T).void }
  def initialize(value)
    @value = value
  end
end

TypedRuby could use a more natural syntax with of:

class Container of T
  def initialize(@value :: T)
  end
  
  def get :: T
    @value
  end
  
  def map of U, &block :: (T) -> U :: Container of U
    Container.new(yield @value)
  end
end

# Usage
container = Container of String with.new("hello")
lengths = container.map { |s| s.length }  # Container of Integer

For multiple type parameters:

class Pair of K, V
  def initialize(@key :: K, @value :: V)
  end
  
  def map_value of U, &block :: (V) -> U :: Pair of K, U
    Pair.new(@key, yield @value)
  end
end
Code language: CSS (css)

Generic Methods

Methods can be generic too:

def identity of T, value :: T :: T
  value
end

def find_first of T, items :: Array of T, &predicate :: (T) -> Boolean :: T?
  items.find(&predicate)
end

# Usage
result = find_first([1, 2, 3, 4]) { |n| n > 2 }  # Integer?
Code language: PHP (php)

Array and Hash Types

Ruby’s arrays and hashes need type support:

# Arrays
numbers :: Array of Integer = [1, 2, 3, 4, 5]
names :: Array of String = ["Alice", "Bob", "Charlie"]

# Or using shorthand
numbers :: [Integer] = [1, 2, 3, 4, 5]
names :: [String] = ["Alice", "Bob", "Charlie"]

# Hashes
user_ages :: Hash of String, Integer = {
  "Alice" => 30,
  "Bob" => 25
}

# Or using shorthand
user_ages :: {String => Integer} = {
  "Alice" => 30,
  "Bob" => 25
}

# Symbol keys (very common in Ruby)
config :: {Symbol => String} = {
  host: "localhost",
  port: "3000"
}
Code language: PHP (php)

Union Types

Ruby’s dynamic nature often uses union types implicitly. Let’s make it explicit:

# TypeScript: string | number
value :: String | Integer = "hello"
value = 42  # OK

# Method with union return type
def find_user(id :: Integer) :: User | nil
  User.find_by(id: id)
end

# Multiple unions
status :: "pending" | "active" | "completed" = "pending"
Code language: PHP (php)

Nullable Types

Ruby uses nil everywhere. TypedRuby needs to handle this elegantly:

# The ? suffix means "or nil"
name :: String? = nil
name = "Ivan"  # OK

# Methods that might return nil
def find_user(id :: Integer) :: User?
  User.find_by(id: id)
end

# Safe navigation works with types
user :: User? = find_user(123)
email = user&.email  # String? inferred
Code language: PHP (php)

Interfaces and Modules

Ruby uses modules for interfaces. TypedRuby could extend this:

interface Comparable of T
  def <=>(other :: T) :: Integer
end

interface Enumerable of T
  def each(&block :: (T) -> void) :: void
end

# Implementation
class User
  include Comparable of User
  
  attr_reader :name :: String
  
  def initialize(@name :: String)
  end
  
  def <=>(other :: User) :: Integer
    name <=> other.name
  end
end
Code language: HTML, XML (xml)

Type Aliases

Creating reusable type definitions:

type UserId = Integer
type Email = String
type UserStatus = "active" | "inactive" | "banned"

type Result of T = 
  { success: true, value: T } |
  { success: false, error: String }

def create_user(name :: String) :: Result of User
  user = User.create(name: name)
  
  if user.persisted?
    { success: true, value: user }
  else
    { success: false, error: user.errors.full_messages.join(", ") }
  end
end
Code language: JavaScript (javascript)

Practical Example: A Repository Pattern

Let’s build something real. Here’s a generic repository in TypedRuby:

interface Repository of T
  def find(id :: Integer) :: T?
  def all :: [T]
  def create(attributes :: Hash) :: T
  def update(id :: Integer, attributes :: Hash) :: T?
  def delete(id :: Integer) :: Boolean
end

class ActiveRecordRepository of T implements Repository of T
  def initialize(@model_class :: Class)
  end
  
  def find(id :: Integer) :: T?
    @model_class.find_by(id: id)
  end
  
  def all :: [T]
    @model_class.all.to_a
  end
  
  def create(attributes :: Hash) :: T
    @model_class.create!(attributes)
  end
  
  def update(id :: Integer, attributes :: Hash) :: T?
    record = find(id)
    return nil unless record
    
    record.update!(attributes)
    record
  end
  
  def delete(id :: Integer) :: Boolean
    record = find(id)
    return false unless record
    
    record.destroy!
    true
  end
end

# Usage
user_repo = ActiveRecordRepository of User .new(User)
users :: [User] = user_repo.all
user :: User? = user_repo.find(123)
Code language: CSS (css)

Blocks and Procs with Types

Blocks are fundamental to Ruby. They need proper type support:

# Block parameter types
def map of T, U, items :: [T], &block :: (T) -> U :: [U]
  items.map(&block)
end

# Proc types
callback :: Proc of (String) -> void = ->(msg) { puts msg }
transformer :: Proc of (Integer) -> String = ->(n) { n.to_s }

# Lambda types
double :: Lambda of (Integer) -> Integer = ->(x) { x * 2 }

# Method that accepts a block with types
def with_timing of T, &block :: () -> T :: T
  start_time = Time.now
  result = yield
  duration = Time.now - start_time
  
  puts "Took #{duration} seconds"
  result
end

# Usage
result :: String = with_timing { expensive_operation() }
Code language: PHP (php)

Rails Integration

Ruby is often Rails. TypedRuby needs to work beautifully with Rails. Here’s where we need to think carefully about syntax. For method calls that take parameters, we can use a generic-style syntax that feels natural.

Generic-style method calls for associations:

class User < ApplicationRecord
  # Using 'of' with method calls (like generic instantiation)
  has_many of Post, :posts
  belongs_to of Company, :company
  has_one of Profile?, :profile
  
  # Or postfix style (reads more naturally)
  has_many :posts of Post
  belongs_to :company of Company
  has_one :profile of Profile?
  
  # For validations, types on the attribute names
  validates :email of String, presence: true, uniqueness: true
  validates :age of Integer, numericality: { greater_than: 0 }
  
  # Scopes with return types
  scope :active of Relation[User], -> { where(status: "active") }
  scope :by_name of Relation[User], ->(name :: String) {
    where("name LIKE ?", "%#{name}%")
  }
  
  # Typed callbacks still use :: for return types
  before_save :normalize_email
  
  def normalize_email :: void
    self.email = email.downcase.strip
  end
  
  # Typed instance methods
  def full_name :: String
    "#{first_name} #{last_name}"
  end
  
  def posts_count :: Integer
    posts.count
  end
end
Code language: HTML, XML (xml)

Alternative: Square bracket syntax (like actual generics):

class User < ApplicationRecord
  # Using square brackets like generic type parameters
  has_many[Post] :posts
  belongs_to[Company] :company
  has_one[Profile?] :profile
  
  # With additional options
  has_many[Post] :posts, dependent: :destroy
  has_many[Comment] :comments, through: :posts
  
  # Validations
  validates[String] :email, presence: true, uniqueness: true
  validates[Integer] :age, numericality: { greater_than: 0 }
  
  # Scopes
  scope[Relation[User]] :active, -> { where(status: "active") }
  scope[Relation[User]] :by_name, ->(name :: String) {
    where("name LIKE ?", "%#{name}%")
  }
end
Code language: HTML, XML (xml)

Comparison of syntaxes:

# Option 1: Postfix 'of' (most Ruby-like)
has_many :posts of Post
validates :email of String, presence: true

# Option 2: Prefix 'of' (generic-like)
has_many of Post, :posts
validates of String, :email, presence: true

# Option 3: Square brackets (actual generics)
has_many[Post] :posts
validates[String] :email, presence: true

# Option 4: 'as:' keyword (traditional keyword argument)
has_many :posts, as: [Post]
validates :email, as: String, presence: true

# Option 5: '<>' Angled brackets (traditional keyword argument)
has_many<[Post]> :posts
validates<String> :email, presence: true
Code language: PHP (php)

I personally prefer Option 2 (prefix ‘of’) because:

  • It reads naturally in English: “has many of Post type”
  • The symbol comes first (Ruby convention)
  • It’s unambiguous and parser-friendly
  • It feels like a natural Ruby extension

Full Rails example with postfix ‘of’:

class User < ApplicationRecord
  has_many :posts of Post, dependent: :destroy
  has_many :comments of Comment, through: :posts
  belongs_to :company of Company
  has_one :profile of Profile?
  
  validates :email of String, presence: true, uniqueness: true
  validates :age of Integer, numericality: { greater_than: 0 }
  validates :status of "active" | "inactive" | "banned", inclusion: { in: %w[active inactive banned] }
  
  scope :active of Relation[User], -> { where(status: "active") }
  scope :by_company of Relation[User], ->(company_id :: Integer) {
    where(company_id: company_id)
  }
  
  before_save :normalize_email
  after_create :send_welcome_email
  
  def normalize_email :: void
    self.email = email.downcase.strip
  end
  
  def full_name :: String
    "#{first_name} #{last_name}"
  end
  
  def recent_posts(limit :: Integer = 10) :: [Post]
    posts.order(created_at: :desc).limit(limit).to_a
  end
end

class PostsController < ApplicationController
  def index :: void
    @posts :: [Post] = Post.includes(:user).order(created_at: :desc)
  end
  
  def show :: void
    @post :: Post = Post.find(params[:id])
  end
  
  def create :: void
    @post :: Post = Post.new(post_params)
    
    if @post.save
      redirect_to @post, notice: "Post created"
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  private
  
  def post_params :: Hash
    params.require(:post).permit(:title, :body, :user_id)
  end
end
Code language: HTML, XML (xml)

How it works under the hood:

The of keyword in method calls would be syntactic sugar that the parser recognizes:

# What you write:
has_many :posts of Post

# What the parser sees:
has_many(:posts, __type__: Post)

# Rails can then use this:
def has_many(name, **options)
  type = options.delete(:__type__)
  
  # Define the association
  define_method(name) do
    # ... normal association logic
  end
  
  # Store type information for runtime validation/documentation
  if type
    association_types[name] = type
    
    # Optional runtime validation in development
    if Rails.env.development?
      define_method(name) do
        result = super()
        validate_type!(result, type)
        result
      end
    end
  end
end
Code language: PHP (php)

This approach:

  • Keeps the symbol first (Ruby convention)
  • Uses familiar of keyword (like we use for generics)
  • Works with all existing parameters
  • Is parser-friendly and unambiguous
  • Reads naturally in English

Complex Example: A Service Object

Let’s build a realistic service object with full type safety:

type TransferResult = 
  { success: true, transaction: Transaction } |
  { success: false, error: String }

class MoneyTransferService
  def initialize(
    @from_account :: Account,
    @to_account :: Account,
    @amount :: BigDecimal
  )
  end
  
  def call :: TransferResult
    return error("Amount must be positive") if @amount <= 0
    return error("Insufficient funds") if @from_account.balance < @amount
    return error("Accounts must be different") if @from_account == @to_account
    
    transaction :: Transaction? = nil
    
    Account.transaction do
      @from_account.withdraw(@amount)
      @to_account.deposit(@amount)
      
      transaction = Transaction.create!(
        from_account: @from_account,
        to_account: @to_account,
        amount: @amount,
        status: "completed"
      )
    end
    
    { success: true, transaction: transaction }
  rescue ActiveRecord::RecordInvalid => e
    error(e.message)
  end
  
  private
  
  def error(message :: String) :: TransferResult
    { success: false, error: message }
  end
end

# Usage
service = MoneyTransferService.new(from_account, to_account, 100.50)
result :: TransferResult = service.call

case result
in { success: true, transaction: tx }
  puts "Transfer successful: #{tx.id}"
in { success: false, error: err }
  puts "Transfer failed: #{err}"
end

Pattern Matching with Types

Ruby 3 introduced pattern matching. TypedRuby makes it type-safe:

type Response of T = 
  { status: "ok", data: T } |
  { status: "error", message: String } |
  { status: "loading" }

def handle_response of T, response :: Response of T :: String
  case response
  in { status: "ok", data: data :: T }
    "Success: #{data}"
  in { status: "error", message: msg :: String }
    "Error: #{msg}"
  in { status: "loading" }
    "Loading..."
  end
end

# Usage
user_response :: Response of User = fetch_user(123)
message = handle_response(user_response)
Code language: PHP (php)

Metaprogramming with Types

Ruby’s metaprogramming is powerful but dangerous. TypedRuby could make it safer:

class Model
  def self.has_typed_attribute of T, name :: Symbol, type :: Class
    define_method(name) :: T do
      instance_variable_get("@#{name}")
    end
    
    define_method("#{name}=") :: void do |value :: T|
      instance_variable_set("@#{name}", value)
    end
  end
end

class User < Model
  has_typed_attribute of String, :name, String
  has_typed_attribute of Integer, :age, Integer
end

user = User.new
user.name = "Ivan"  # OK
user.age = 30       # OK
user.name = 123     # Type error!
Code language: HTML, XML (xml)

Gradual Typing

The beauty of TypedRuby is that it’s optional. You can mix typed and untyped code:

# Completely untyped (classic Ruby)
def process(data)
  data.map { |x| x * 2 }
end

# Partially typed
def process(data :: Array)
  data.map { |x| x * 2 }
end

# Fully typed
def process of T, data :: [T], &block :: (T) -> T :: [T]
  data.map(&block)
end

# The three can coexist in the same codebase
Code language: PHP (php)

Type System and Object Hierarchy

Here’s a crucial question: how do types relate to Ruby’s object system? In Ruby, everything is an object, and every class inherits from Object (or BasicObject). TypedRuby’s type system needs to respect this.

Types ARE classes (mostly)

In TypedRuby, most types would literally be the classes themselves:

# String is both a class and a type
name :: String = "Ivan"
puts String.class  # => Class
puts String.ancestors  # => [String, Comparable, Object, Kernel, BasicObject]

# User is both a class and a type
user :: User = User.new
puts User.class  # => Class
puts User.ancestors  # => [User, ApplicationRecord, ActiveRecord::Base, Object, ...]

This is fundamentally different from TypeScript, where types exist only at compile time. In TypedRuby, types are runtime objects too.

Special type constructors

Some type syntax creates type objects at runtime:

# Array type constructor
posts :: [Post] = []

# This is roughly equivalent to:
posts :: Array[Post] = []

# Which could be implemented as:
class Array
  def self.[](element_type)
    TypedArray.new(element_type)
  end
end

# Hash type constructor
ages :: {String => Integer} = {}

# Roughly:
ages :: Hash[String, Integer] = {}

The Type class hierarchy

TypedRuby would introduce a parallel type hierarchy:

# New base classes for type system
class Type
  # Base class for all types
end

class GenericType < Type
  # For parameterized types like Array[T], Hash[K,V]
  attr_reader :type_params
  
  def initialize(*type_params)
    @type_params = type_params
  end
end

class UnionType < Type
  # For union types like String | Integer
  attr_reader :types
  
  def initialize(*types)
    @types = types
  end
end

class NullableType < Type
  # For nullable types like String?
  attr_reader :inner_type
  
  def initialize(inner_type)
    @inner_type = inner_type
  end
end

# These would be used like:
array_of_posts = GenericType.new(Array, Post)  # [Post]
string_or_int = UnionType.new(String, Integer)  # String | Integer
nullable_user = NullableType.new(User)  # User?
Code language: CSS (css)

Runtime type checking

Because types are objects, you could check them at runtime:

def process(value :: String | Integer)
  case value
  when String
    value.upcase
  when Integer
    value * 2
  end
end

# The type annotation creates a runtime check:
def process(value)
  # Compiler inserts:
  unless value.is_a?(String) || value.is_a?(Integer)
    raise TypeError, "Expected String | Integer, got #{value.class}"
  end
  
  case value
  when String
    value.upcase
  when Integer
    value * 2
  end
end
Code language: PHP (php)

Type as values (reflection)

Types being objects means you can work with them:

def type_info of T, value :: T :: Hash
  {
    value: value,
    type: T,
    class: value.class,
    ancestors: T.ancestors
  }
end

result = type_info("hello")
puts result[:type]  # => String
puts result[:class]  # => String
puts result[:ancestors]  # => [String, Comparable, Object, ...]

# Generic types are objects too:
array_type = Array of String
puts array_type.class  # => GenericType
puts array_type.type_params  # => [String]

Method objects with type information

Ruby’s Method objects could expose type information:

class User
  def greet(name :: String) :: String
    "Hello, #{name}"
  end
end

method = User.instance_method(:greet)
puts method.parameter_types  # => [String]
puts method.return_type  # => String

# This enables runtime validation:
def call_safely(obj, method_name, *args)
  method = obj.method(method_name)
  
  # Check argument types
  method.parameter_types.each_with_index do |type, i|
    unless args[i].is_a?(type)
      raise TypeError, "Argument #{i} must be #{type}"
    end
  end
  
  obj.send(method_name, *args)
end

Duck typing still works

Even with types, Ruby’s duck typing philosophy is preserved:

# You can still use duck typing without types
def quack(duck)
  duck.quack
end

# Or enforce types when you want safety
def quack(duck :: Duck) :: String
  duck.quack
end

# Or use interfaces for structural typing
interface Quackable
  def quack :: String
end

def quack(duck :: Quackable) :: String
  duck.quack  # Works with any object that implements quack
end
Code language: CSS (css)

Type compatibility and inheritance

Types follow Ruby’s inheritance rules:

class Animal
  def speak :: String
    "Some sound"
  end
end

class Dog < Animal
  def speak :: String
    "Woof"
  end
end

# Dog is a subtype of Animal
def make_speak(animal :: Animal) :: String
  animal.speak
end

dog = Dog.new
make_speak(dog)  # OK, Dog < Animal

# Liskov Substitution Principle applies
animals :: [Animal] = [Dog.new, Cat.new, Bird.new]

The as: keyword and runtime behavior

When you write:

has_many :posts, as: [Post]
Code language: CSS (css)

This could be expanded by the Rails framework to:

has_many :posts, type_checker: -> (value) {
  value.is_a?(Array) && value.all? { |item| item.is_a?(Post) }
}
Code language: JavaScript (javascript)

Rails could use this for runtime validation in development mode, giving you immediate feedback if you accidentally assign the wrong type.

Performance considerations

Runtime type checking has overhead. TypedRuby could handle this smartly:

# In development/test: full runtime checking
ENV['RUBY_TYPE_CHECKING'] = 'strict'

# In production: types checked only at compile time
ENV['RUBY_TYPE_CHECKING'] = 'none'

# Or selective checking for critical paths
ENV['RUBY_TYPE_CHECKING'] = 'public_apis'
Code language: PHP (php)

Integration with existing Ruby

Since types are objects, they integrate seamlessly:

# Works with reflection
User.instance_methods.each do |method|
  m = User.instance_method(method)
  if m.respond_to?(:return_type)
    puts "#{method} returns #{m.return_type}"
  end
end

# Works with metaprogramming
class User
  [:name, :email, :age].each do |attr|
    define_method(attr) :: String do
      instance_variable_get("@#{attr}")
    end
  end
end

# Works with monkey patching (for better or worse)
class String
  def original_upcase :: String
    # Type information is preserved
  end
end

This approach makes TypedRuby feel like a natural evolution of Ruby rather than a foreign type system bolted on. Types are just objects, following Ruby’s “everything is an object” philosophy.

TypedRuby should infer types aggressively:

# Inferred from literal
name = "Ivan"  # String inferred

# Inferred from method return
def get_age
  30
end

age = get_age  # Integer inferred

# Inferred from array contents
numbers = [1, 2, 3, 4]  # [Integer] inferred

# Inferred from hash
user = {
  name: "Ivan",
  age: 30,
  active: true
}  # {Symbol => String | Integer | Boolean} inferred

# Explicit typing when inference isn't enough
mixed :: [Integer | String] = [1, "two", 3]
Code language: PHP (php)

Why This Could Work

Unlike Sorbet and RBS, TypedRuby would be:

  1. Native: Types are part of the language syntax, not bolted on
  2. Optional: You choose where to add types
  3. Gradual: Mix typed and untyped code freely
  4. Readable: Syntax feels like Ruby, not like Java
  5. Powerful: Full generics, unions, intersections, pattern matching
  6. Practical: Works with Rails, metaprogramming, blocks, procs

The syntax respects Ruby’s philosophy. It’s minimal, expressive, and doesn’t get in your way. When you want types, they’re there. When you don’t, they’re not.

The Implementation Challenge

Could this be built? Technically, yes. You’d need to:

  1. Extend the Ruby parser to recognize type annotations
  2. Build a type checker that understands Ruby’s semantics
  3. Make it work with Ruby’s dynamic features
  4. Integrate with existing tools (RuboCop, RubyMine, VS Code)
  5. Handle the massive existing Ruby ecosystem

The hard part isn’t the syntax. It’s making the type checker smart enough to handle Ruby’s dynamism while still being useful. Ruby’s metaprogramming, method_missing, dynamic dispatch, these all make static typing hard.

But not impossible. Crystal proved you can have Ruby-like syntax with static types. Sorbet proved you can add types to Ruby code. TypedRuby would combine the best of both: native syntax with gradual typing.

The Dream

Imagine opening a Rails codebase and seeing:

class User < ApplicationRecord
  has_many :posts :: [Post]
  
  def full_name :: String
    "#{first_name} #{last_name}"
  end
end

class PostsController < ApplicationController
  def create :: void
    @post :: Post = Post.new(post_params)
    @post.save!
    redirect_to @post
  end
end

The types are there when you need them, documenting the code and catching bugs. But they don’t dominate. The code still looks like Ruby. It still feels like Ruby.

That’s what TypedRuby could be. Not a separate type system bolted onto Ruby. Not a different language inspired by Ruby. But Ruby itself, evolved to support the type safety modern developers expect.

Would It Succeed?

Honestly? Probably not. Ruby’s community values dynamism and flexibility. Matz has explicitly said he doesn’t want mandatory typing. The ecosystem is built on duck typing and metaprogramming.

But that doesn’t mean it wouldn’t be useful. A significant portion of Ruby developers would adopt optional typing if it felt natural. Rails applications would benefit from type safety in controllers, models, and services. API clients would be more reliable. Refactoring would be safer.

The key is making it optional and making it Ruby. Not Sorbet’s verbose sig blocks. Not RBS’s separate files. Just Ruby, with types when you want them.

Conclusion

TypedRuby is a thought experiment, but it’s a valuable one. It shows what’s possible when you design types into a language from the start, rather than bolting them on later.

Ruby is beautiful. Types don’t have to ruin that beauty. With the right syntax, the right philosophy, and the right implementation, they could enhance it.

Maybe someday we’ll see Ruby 4.0 with native, optional type annotations. Maybe we won’t. But it’s fun to imagine a world where Ruby has the expressiveness we love and the type safety we need.

Until then, we have Sorbet and RBS. They’re not perfect, but they’re what we’ve got. And who knows? Maybe they’ll evolve. Maybe the syntax will improve. Maybe they’ll feel more Ruby-like over time.

Or maybe someone will read this and decide to build TypedRuby for real.

A developer can dream.

TypedScript: Imagining CoffeeScript with Types

The content envisions a hypothetical programming language called “TypedScript,” merging the elegance of CoffeeScript with TypeScript’s type safety. It advocates for optional types, clean syntax, aggressive type inference, and elegance in generics, while maintaining CoffeeScript’s aesthetic. The idea remains theoretical, noting practical challenges with adoption in the current ecosystem.

After writing my love letter to CoffeeScript, I couldn’t stop thinking: what if CoffeeScript had embraced types instead of fading away? What if someone had built a typed version that kept all the syntactic elegance while adding the type safety that makes TypeScript so powerful?

Let’s imagine that world. Let’s design what I’ll call “TypedScript” (or maybe CoffeeType? TypedCoffee? We’ll workshop the name). The goal: keep everything that made CoffeeScript beautiful while adding first-class support for types and generics.

The Core Principles

Before we dive into syntax, let’s establish what we’re trying to achieve:

  1. Types should be optional but encouraged. You can write untyped code and gradually add types.
  2. Syntax should stay clean. No angle brackets everywhere, no visual noise.
  3. Type inference should be aggressive. The compiler should figure out as much as possible.
  4. Generics should be elegant. No <T, U, V> mess.
  5. The Ruby/Python aesthetic must be preserved. Significant whitespace, minimal punctuation, readable code.

Basic Type Annotations

Let’s start simple. In TypeScript, you write:

const name: string = "Ivan";
const age: number = 30;
const isActive: boolean = true;
Code language: JavaScript (javascript)

In TypedScript, I’d imagine:

name: String = "Ivan"
age: Number = 30
isActive: Boolean = true
Code language: JavaScript (javascript)

Or with type inference (which should work most of the time):

name = "Ivan"        # inferred as String
age = 30             # inferred as Number
isActive = true      # inferred as Boolean
Code language: PHP (php)

The colon for type annotations feels natural. It’s what TypeScript uses, and it doesn’t clash with CoffeeScript’s existing syntax.

Function Signatures

TypeScript function types can get verbose:

function greet(name: string, age?: number): string {
  return age 
    ? `Hello ${name}, you are ${age}` 
    : `Hello ${name}`;
}

const add = (a: number, b: number): number => a + b;
Code language: JavaScript (javascript)

TypedScript could look like this:

greet = (name: String, age?: Number) -> String
  if age?
    "Hello #{name}, you are #{age}"
  else
    "Hello #{name}"

add = (a: Number, b: Number) -> Number
  a + b
Code language: JavaScript (javascript)

Even cleaner with inference:

greet = (name: String, age?: Number) ->
  if age?
    "Hello #{name}, you are #{age}"
  else
    "Hello #{name}"

add = (a: Number, b: Number) -> a + b
Code language: JavaScript (javascript)

The return type is inferred from the actual return value. This is already how CoffeeScript works (implicit returns), so we just layer types on top.

Interfaces and Type Definitions

TypeScript interfaces are pretty clean, but they still require curly braces:

interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
  roles: string[];
}
Code language: PHP (php)

In TypedScript, we could use indentation:

type User
  id: String
  name: String
  email: String
  age?: Number
  roles: [String]
Code language: JavaScript (javascript)

Or for inline types:

user: {id: String, name: String, email: String}
Code language: CSS (css)

Arrays could use the Ruby-inspired [Type] syntax. Tuples could be [String, Number]. Maps could be {String: User}.

Classes with Types

TypeScript classes are already pretty good, but they’re still verbose:

class UserService {
  private users: User[] = [];
  
  constructor(private apiClient: ApiClient) {}
  
  async getUser(id: string): Promise<User> {
    const response = await this.apiClient.get(`/users/${id}`);
    return response.data;
  }
  
  addUser(user: User): void {
    this.users.push(user);
  }
}
Code language: JavaScript (javascript)

TypedScript version:

class UserService
  users: [User] = []
  
  constructor: (@apiClient: ApiClient) ->
  
  getUser: (id: String) -> Promise<User>
    response = await @apiClient.get "/users/#{id}"
    response.data
  
  addUser: (user: User) -> Void
    @users.push user
Code language: HTML, XML (xml)

The @ syntax for instance variables is preserved, and we just add type annotations where needed. Constructor parameter properties (@apiClient: ApiClient) combine declaration and assignment in one elegant line.

Generics: The Tricky Part

This is where TypeScript gets ugly. Generics in TypeScript look like this:

class Container<T> {
  private value: T;
  
  constructor(value: T) {
    this.value = value;
  }
  
  map<U>(fn: (value: T) => U): Container<U> {
    return new Container(fn(this.value));
  }
}

function identity<T>(value: T): T {
  return value;
}

const result = identity<string>("hello");
Code language: JavaScript (javascript)

The angle brackets are noisy, and they clash with comparison operators. TypedScript needs a different approach. What if we used a more natural syntax inspired by mathematical notation?

class Container of T
  value: T
  
  constructor: (@value: T) ->
  
  map: (fn: (T) -> U) -> Container of U for any U
    new Container fn(@value)

identity = (value: T) -> T for any T
  value

result = identity "hello"  # type inferred

The of keyword introduces type parameters for classes. The for any T suffix introduces type parameters for functions. When calling generic functions, types are inferred automatically in most cases.

For multiple type parameters:

class Pair of K, V
  constructor: (@key: K, @value: V) ->
  
  map: (fn: (V) -> U) -> Pair of K, U for any U
    new Pair @key, fn(@value)

Union Types and Intersections

TypeScript uses | for unions and & for intersections:

type Result = Success | Error;
type Employee = Person & Worker;
Code language: JavaScript (javascript)

TypedScript could keep this, but make it more readable:

type Result = Success | Error

type Employee = Person & Worker

# Or with more complex types
type Response = 
  | {status: "success", data: User}
  | {status: "error", message: String}
Code language: PHP (php)

Advanced Generic Constraints

TypeScript has complex generic constraints:

function findMax<T extends Comparable>(items: T[]): T {
  return items.reduce((max, item) => 
    item.compareTo(max) > 0 ? item : max
  );
}
Code language: JavaScript (javascript)

In TypedScript:

findMax = (items: [T]) -> T for any T extends Comparable
  items.reduce (max, item) ->
    if item.compareTo(max) > 0 then item else max
Code language: JavaScript (javascript)

Practical Example: Building a Generic Repository

Let’s build something real. Here’s a TypeScript generic repository:

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

class ApiRepository<T> implements Repository<T> {
  constructor(
    private endpoint: string,
    private client: HttpClient
  ) {}
  
  async findById(id: string): Promise<T | null> {
    try {
      const response = await this.client.get(`${this.endpoint}/${id}`);
      return response.data;
    } catch (error) {
      return null;
    }
  }
  
  async findAll(): Promise<T[]> {
    const response = await this.client.get(this.endpoint);
    return response.data;
  }
  
  async save(entity: T): Promise<T> {
    const response = await this.client.post(this.endpoint, entity);
    return response.data;
  }
  
  async delete(id: string): Promise<void> {
    await this.client.delete(`${this.endpoint}/${id}`);
  }
}
Code language: JavaScript (javascript)

The TypedScript version:

interface Repository of T
  findById: (id: String) -> Promise<T?>
  findAll: () -> Promise<[T]>
  save: (entity: T) -> Promise<T>
  delete: (id: String) -> Promise<Void>

class ApiRepository of T implements Repository of T
  constructor: (@endpoint: String, @client: HttpClient) ->
  
  findById: (id: String) -> Promise<T?>
    try
      response = await @client.get "#{@endpoint}/#{id}"
      response.data
    catch error
      null
  
  findAll: () -> Promise<[T]>
    response = await @client.get @endpoint
    response.data
  
  save: (entity: T) -> Promise<T>
    response = await @client.post @endpoint, entity
    response.data
  
  delete: (id: String) -> Promise<Void>
    await @client.delete "#{@endpoint}/#{id}"

# Usage
userRepo = new ApiRepository of User "users", httpClient
users = await userRepo.findAll()
Code language: HTML, XML (xml)

Look at how clean that is. No angle brackets, no semicolons, no excessive braces. The type information is there, but it doesn’t dominate the code.

Type Guards and Narrowing

TypeScript’s type guards work well:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

if (isString(data)) {
  console.log(data.toUpperCase());
}
Code language: JavaScript (javascript)

TypedScript could use a similar pattern:

isString = (value: Unknown) -> value is String
  typeof value == "string"

if isString data
  console.log data.toUpperCase()
Code language: JavaScript (javascript)

Utility Types

TypeScript has utility types like Partial<T>, Pick<T, K>, Omit<T, K>. These could work in TypedScript with a more natural syntax:

# TypeScript
type PartialUser = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;

# TypedScript
type PartialUser = Partial of User
type UserPreview = Pick of User, "id" | "name"
type UserWithoutEmail = Omit of User, "email"
Code language: PHP (php)

The Existential Operator with Types

Remember CoffeeScript’s beloved ? operator? It would work beautifully with nullable types:

user: User? = await findUser id  # User | null

name = user?.name ? "Guest"
user?.profile?.update()
callback?()
Code language: PHP (php)

The ? in User? means nullable, just like TypeScript’s User | null or User | undefined.

Real-World Example: A Todo App

Let’s put it all together with a realistic example:

type Todo
  id: String
  title: String
  completed: Boolean
  createdAt: Date

type TodoFilter = "all" | "active" | "completed"

class TodoStore
  todos: [Todo] = []
  filter: TodoFilter = "all"
  
  constructor: (@storage: Storage) ->
    @loadTodos()
  
  loadTodos: () -> Void
    data = @storage.get "todos"
    @todos = if data? then JSON.parse data else []
  
  saveTodos: () -> Void
    @storage.set "todos", JSON.stringify @todos
  
  addTodo: (title: String) -> Todo
    todo: Todo =
      id: generateId()
      title: title
      completed: false
      createdAt: new Date()
    
    @todos.push todo
    @saveTodos()
    todo
  
  toggleTodo: (id: String) -> Boolean
    todo = @todos.find (t) -> t.id == id
    return false unless todo?
    
    todo.completed = !todo.completed
    @saveTodos()
    true
  
  deleteTodo: (id: String) -> Boolean
    index = @todos.findIndex (t) -> t.id == id
    return false if index == -1
    
    @todos.splice index, 1
    @saveTodos()
    true
  
  getFilteredTodos: () -> [Todo]
    switch @filter
      when "active" then @todos.filter (t) -> !t.completed
      when "completed" then @todos.filter (t) -> t.completed
      else @todos

generateId = () -> String
  Math.random().toString(36).substr 2, 9

Compare that to the TypeScript equivalent and tell me it isn’t more elegant. The types are there, providing safety and documentation, but they don’t overwhelm the code. You can still read it naturally.

Why This Matters

TypeScript won because it added types to JavaScript without fundamentally changing the language. That was smart from an adoption standpoint. But it meant keeping JavaScript’s verbose syntax.

If TypedScript had existed, we could have had both: the elegance of CoffeeScript and the safety of TypeScript. We could write code that’s both beautiful and robust.

The tragedy is that this never happened. CoffeeScript’s creator, Jeremy Ashkenas, explicitly rejected adding types. He felt they went against CoffeeScript’s philosophy of simplicity. Meanwhile, TypeScript embraced JavaScript’s syntax for compatibility.

Could This Still Happen?

Technically, someone could build this. The CoffeeScript compiler is open source. TypeScript’s type system is well-documented. A sufficiently motivated team could fork CoffeeScript and add a type system.

But would anyone use it? Probably not. The JavaScript ecosystem has moved on. TypeScript has won. The tooling, the community, the momentum are all there. Starting a new compile-to-JavaScript language in 2025 would be fighting an uphill battle.

Still, it’s fun to imagine. And who knows? Maybe in some parallel universe, TypedScript is the dominant language for web development, and developers there are writing beautiful, type-safe code that makes our TypeScript look verbose and clunky.

A developer can dream.

The Syntax Reference

For anyone curious, here’s a quick reference of what TypedScript syntax could look like:

# Basic types
name: String = "Ivan"
age: Number = 30
active: Boolean = true
data: Any = anything()
nothing: Void = undefined

# Arrays and tuples
numbers: [Number] = [1, 2, 3]
tuple: [String, Number] = ["Ivan", 30]

# Objects
user: {name: String, age: Number} = {name: "Ivan", age: 30}

# Nullable types
optional: String? = null

# Union types
status: "pending" | "active" | "complete" = "pending"
value: String | Number = 42

# Functions
greet: (name: String) -> String = (name) -> "Hello #{name}"

# Generic functions
identity = (value: T) -> T for any T
  value

# Generic classes
class Container of T
  value: T
  constructor: (@value: T) ->

# Interfaces
interface Comparable of T
  compareTo: (other: T) -> Number

# Type aliases
type UserId = String
type Result of T = {ok: true, value: T} | {ok: false, error: String}

# Constraints
sorted = (items: [T]) -> [T] for any T extends Comparable of T
  items.sort (a, b) -> a.compareTo b

Closing Thoughts

Would TypedScript be better than TypeScript? For me, yes. The cleaner syntax, the Ruby-inspired aesthetics, the focus on readability, all while keeping the benefits of static typing. It would be the best of both worlds.

But “better” is subjective. TypeScript’s compatibility with JavaScript is a huge advantage. Its massive ecosystem is irreplaceable. Its tooling is mature and battle-tested.

TypedScript would be a beautiful language that few people use. And maybe that’s okay. Not every good idea wins. Sometimes the practical choice beats the elegant one.

But I still wish I could write my production code in TypedScript. I think it would be a joy.

What do you think? Would you use TypedScript if it existed? What syntax choices would you make differently? Let me know in the comments.

A Love Letter to CoffeeScript and HAML: When Rails Frontend Development Was Pure Joy

The author reflects on the nostalgia of older coding practices, specifically with Ruby on Rails, CoffeeScript, and HAML. They appreciate the simplicity, conciseness, and readability of these technologies compared to modern alternatives like TypeScript. While acknowledging TypeScript’s superiority in type safety, they express a longing for the elegant developer experience of the past.

There’s something bittersweet about looking back at old codebases. Recently, I found myself diving into a Ruby on Rails project from 2012, and I was immediately transported back to an era when frontend development felt different. Better, even. The stack was CoffeeScript, HAML, and Rails’ asset pipeline, and you know what? It was glorious.

I know what you’re thinking. “CoffeeScript? That thing died years ago. TypeScript won. Get over it.” And you’re right. TypeScript did win. It’s everywhere now, and for good reasons. But let me tell you why, after all these years, I still get a little nostalgic pang when I think about writing CoffeeScript, and why part of me still thinks it was the better language.

The Rails Way: Opinionated and Proud

First, let’s set the scene. This was the golden age of Rails, when “convention over configuration” wasn’t just a tagline. It was a philosophy that permeated everything. The asset pipeline handled all your JavaScript and CSS compilation. You’d drop a .coffee file in app/assets/javascripts, write your code, and Rails would handle the rest. No webpack configs, no Babel presets, no decision fatigue about which bundler to use.

Your views lived in HAML files that looked like this:

.user-profile
  .header
    %h1= @user.name
    %p.bio= @user.bio
  
  .actions
    = link_to "Edit Profile", edit_user_path(@user), class: "btn btn-primary"
    = link_to "Delete Account", user_path(@user), method: :delete, 
      data: { confirm: "Are you sure?" }, class: "btn btn-danger"
Code language: JavaScript (javascript)

And your JavaScript looked like this:

class UserProfile
  constructor: (@element) ->
    @setupEventListeners()
  
  setupEventListeners: ->
    @element.find('.btn-danger').on 'click', (e) =>
      @handleDelete(e)
  
  handleDelete: (e) ->
    return unless confirm('Really delete?')
    
    $.ajax
      url: $(e.target).attr('href')
      method: 'DELETE'
      success: => @onDeleteSuccess()
      error: => @onDeleteError()
  
  onDeleteSuccess: ->
    @element.fadeOut()
    Notifications.show 'Account deleted successfully'

$ ->
  $('.user-profile').each ->
    new UserProfile($(this))
Code language: CSS (css)

Look at that. It’s beautiful. It’s concise. It’s expressive. And it just works.

Why HAML Was a Breath of Fresh Air

Let’s talk about HAML first. If you’ve never used it, HAML (HTML Abstraction Markup Language) was a templating language that let you write HTML without all the angle brackets. Instead of this:

<div class="container">
  <div class="row">
    <div class="col-md-6">
      <h1>Welcome</h1>
      <p class="lead">This is my website</p>
    </div>
  </div>
</div>
Code language: HTML, XML (xml)

You wrote this:

.container
  .row
    .col-md-6
      %h1 Welcome
      %p.lead This is my website
Code language: CSS (css)

The difference is striking. HAML forced you to write clean, properly indented markup. You couldn’t forget to close a tag because there were no closing tags. The structure was defined by indentation, Python-style. This meant your templates were always consistently formatted, always readable, and always correctly nested.

HAML also integrated beautifully with Ruby. Want to interpolate a variable? Just use =. Want to add a conditional? Use standard Ruby syntax. The mental model was simple: it’s just Ruby that outputs HTML.

- if current_user.admin?
  .admin-panel
    %h2 Admin Controls
    = render partial: 'admin/controls'
- else
  .user-message
    %p You don't have access to this section.
Code language: PHP (php)

No context switching between template syntax and programming language syntax. It was all Ruby, all the way down.

CoffeeScript: JavaScript for People Who Don’t Like JavaScript

Now, let’s get to the controversial part: CoffeeScript. For those who missed it, CoffeeScript was a language that compiled to JavaScript, created by Jeremy Ashkenas in 2009. It took heavy inspiration from Ruby and Python, offering a cleaner syntax that eliminated much of JavaScript’s syntactic noise.

Here’s the thing people forget: JavaScript in 2011 was terrible. No modules, no classes, no arrow functions, no destructuring, no template strings, no const or let. You had var, function expressions, and pain. So much pain.

CoffeeScript gave us:

Arrow functions (before ES6):

numbers = [1, 2, 3, 4, 5]
doubled = numbers.map (n) -> n * 2

Class syntax (before ES6):

class Animal
  constructor: (@name) ->
  
  speak: ->
    console.log "#{@name} makes a sound"

class Dog extends Animal
  speak: ->
    console.log "#{@name} barks"
Code language: CSS (css)

String interpolation (before ES6):

name = "Ivan"
greeting = "Hello, #{name}!"
Code language: JavaScript (javascript)

Destructuring (before ES6):

{name, age} = user
[first, second, rest...] = numbers

Comprehensions (still not in JavaScript):

adults = (person for person in people when person.age >= 18)

CoffeeScript didn’t just add syntax sugar. It changed how you thought about JavaScript. The code was more expressive, more concise, and more Ruby-like. For Rails developers, it felt like home.

The Magic of the Asset Pipeline

What made this stack truly shine was how it all fit together. The Rails asset pipeline was like magic. Possibly black magic, but magic nonetheless.

You’d organize your code like this:

app/assets/
  javascripts/
    application.coffee
    models/
      user.coffee
      post.coffee
    views/
      users/
        profile.coffee
      posts/
        index.coffee

In your application.coffee, you’d require your dependencies:

#= require jquery
#= require jquery_ujs
#= require_tree ./models
#= require_tree ./views
Code language: PHP (php)

Rails would automatically compile everything, concatenate it in the right order, minify it for production, and serve it with cache-busting fingerprints. You didn’t think about build tools. You just wrote code.

The same applied to stylesheets. Drop a .scss file in app/assets/stylesheets, and it would be compiled and served. Want to use a gem that includes assets? Add it to your Gemfile, and its assets would automatically be available. No CDN links, no manual script tags.

Was it perfect? No. Was it sometimes confusing when assets weren’t loading in the order you expected? Yes. But the developer experience was smooth. You could go from idea to implementation incredibly quickly.

Why CoffeeScript Still Feels Better Than TypeScript

Okay, here’s where I’m going to lose some of you. TypeScript is objectively the right choice for modern JavaScript development. It has type safety, incredible tooling, massive community support, and it’s actively developed by Microsoft. CoffeeScript is essentially dead, with minimal updates and a dwindling community.

And yet… CoffeeScript still feels better to write.

Let me explain. TypeScript added types to JavaScript, which is fantastic. But it kept JavaScript’s verbose syntax. You still have curly braces everywhere, you still need semicolons (or don’t, which becomes its own debate), you still have visual noise.

Compare these:

TypeScript:

interface User {
  name: string;
  email: string;
  age: number;
}

class UserService {
  constructor(private apiClient: ApiClient) {}
  
  async getUser(id: string): Promise<User> {
    const response = await this.apiClient.get(`/users/${id}`);
    return response.data;
  }
  
  filterAdults(users: User[]): User[] {
    return users.filter((user) => user.age >= 18);
  }
}
Code language: JavaScript (javascript)

CoffeeScript:

class UserService
  constructor: (@apiClient) ->
  
  getUser: (id) ->
    response = await @apiClient.get "/users/#{id}"
    response.data
  
  filterAdults: (users) ->
    users.filter (user) -> user.age >= 18
Code language: CSS (css)

The CoffeeScript version is cleaner. There’s less visual noise, less ceremony. The @ symbol for instance properties is brilliant. It’s immediately obvious what’s a property and what’s a local variable. The implicit returns mean you’re not constantly writing return statements. The significant whitespace enforces good formatting.

Yes, TypeScript gives you type safety. That’s huge. But CoffeeScript gives you readability. And in my experience, readable code is maintainable code. I can glance at CoffeeScript and immediately understand what it’s doing. TypeScript requires more parsing, more mental overhead.

The list comprehensions in CoffeeScript are particularly beautiful:

# CoffeeScript
evenSquares = (n * n for n in numbers when n % 2 == 0)

# TypeScript
const evenSquares = numbers
  .filter((n) => n % 2 === 0)
  .map((n) => n * n);
Code language: PHP (php)

Both work, but the CoffeeScript version reads like English: “n squared for each n in numbers when n is even.” It’s declarative and expressive.

The Existential Operator: A Love Story

One of CoffeeScript’s best features was the existential operator (?). It was like optional chaining before optional chaining existed, but more powerful:

# Safe property access
name = user?.profile?.name

# Default values
speed = options?.speed ? 75

# Function existence check
callback?()

# Existence assignment
value ?= "default"
Code language: PHP (php)

That last one, ?=, was particularly great. It means “assign if the variable is null or undefined.” It’s cleaner than value = value || "default" and more correct (because it doesn’t overwrite falsy-but-valid values like 0 or "").

TypeScript eventually got optional chaining (?.) and nullish coalescing (??), which is great. But it took years, and CoffeeScript had it from the start.

What We Lost

When the JavaScript community moved from CoffeeScript to ES6 and then TypeScript, we gained a lot. Type safety, better tooling, standardization. But we also lost something.

We lost the joy of writing concise code. We lost the elegance of Ruby-inspired syntax. We lost the community that valued readability and expressiveness over completeness and type safety.

Modern JavaScript development often feels like you’re fighting the tools. Configuring TypeScript, setting up ESLint, configuring Prettier, choosing between competing libraries, debugging sourcemaps, dealing with module resolution issues. It’s powerful, but it’s exhausting.

With CoffeeScript and Rails, you just wrote code. The decisions were made for you. The tools were integrated. The conventions were clear. It was opinionated, and that was a feature, not a bug.

The Verdict

Would I start a new project with CoffeeScript and HAML today? Probably not. The ecosystem has moved on. TypeScript has won, and for most use cases, it’s the right choice. React and Vue have replaced server-rendered templates. The world has changed.

But do I miss it? Absolutely.

I miss the simplicity. I miss the elegance. I miss being able to write beautiful, concise code without worrying about types and interfaces and generics. I miss HAML’s clean markup and the way it forced you to write good HTML. I miss the Rails asset pipeline just working without configuration.

Most of all, I miss the developer experience of that era. We were moving fast, building things quickly, and having fun doing it. The code was readable, the stack was coherent, and everything felt like it fit together.

Maybe that’s just nostalgia talking. Maybe I’m romanticizing the past and forgetting the pain points. But when I look at that old CoffeeScript code, I don’t see technical debt. I see craft. I see code that was written with care, that values clarity over cleverness, that respects the reader’s time.

And honestly? I still think CoffeeScript’s syntax is better. TypeScript is more powerful, more practical, and more maintainable at scale. But CoffeeScript is more beautiful.

Sometimes, that matters too.


What are your thoughts? Did you work with CoffeeScript and HAML back in the day? Do you miss them, or are you glad we’ve moved on? Let me know in the comments or reach out on Twitter.

Rails Templating Showdown: Slim vs ERB vs Haml vs Phlex – Which One Should You Use?

This guide compares Ruby on Rails templating engines: ERB, Slim, Haml, and Phlex. It highlights each engine’s pros and cons, focusing on aspects like performance, readability, and learning curve. Recommendations are made based on project type, emphasizing the importance of choosing the right engine for optimal efficiency and maintainability.

If you’ve been working with Ruby on Rails for any length of time, you’ve probably encountered the age-old question: which templating engine should I use? With ERB as the default, Slim and Haml as popular alternatives, and Phlex as the new kid on the block, the choice can feel overwhelming.

In this comprehensive guide, I’ll break down each option, compare their strengths and weaknesses, and help you make an informed decision for your Rails projects.

Understanding the Landscape

Before diving into specifics, let’s understand what we’re comparing. Template engines are tools that help you generate HTML dynamically by embedding Ruby code within markup. Each engine has a different philosophy about how this should be done.

ERB (Embedded Ruby)

What is it? ERB is Rails’ default templating engine. It embeds Ruby code directly into HTML using special tags.

Syntax Example

<div class="user-profile">
  <h1><%= @user.name %></h1>
  <% if @user.admin? %>
    <span class="badge">Admin</span>
  <% end %>
  <ul class="posts">
    <% @user.posts.each do |post| %>
      <li><%= link_to post.title, post_path(post) %></li>
    <% end %>
  </ul>
</div>
Code language: HTML, XML (xml)

Pros

Zero Learning Curve: If you know HTML and Ruby, you already know ERB. There’s no new syntax to learn, making it perfect for beginners and mixed teams.

Universal Support: Every Rails developer knows ERB. Every gem, tutorial, and Stack Overflow answer uses ERB. This ubiquity is valuable.

No Setup Required: It works out of the box with every Rails installation. No gems to add, no configuration needed.

Familiar to Other Ecosystems: The concept of embedding code in angle brackets exists in PHP, ASP, JSP, and many other frameworks. Developers coming from other backgrounds will feel at home.

Cons

Verbose: Writing closing tags for everything gets tedious. Your files become longer than they need to be.

Easy to Create Messy Code: Because ERB doesn’t enforce structure, it’s easy to mix business logic with presentation logic, leading to hard-to-maintain views.

Repetitive: You’ll find yourself typing the same patterns over and over. The lack of shortcuts makes ERB feel inefficient once you’ve experienced alternatives.

When to Use ERB

ERB is ideal when you’re starting a new project with junior developers, working with a team that values convention over optimization, or building simple CRUD applications where template complexity is minimal. It’s also the safe choice for open-source projects where maximum accessibility matters.

Slim

What is it? Slim is a lightweight templating engine focused on reducing syntax to its bare essentials. Its motto is “what’s left when you take the fat off ERB.”

Syntax Example

.user-profile
  h1 = @user.name
  - if @user.admin?
    span.badge Admin
  ul.posts
    - @user.posts.each do |post|
      li = link_to post.title, post_path(post)
Code language: JavaScript (javascript)

Pros

Dramatically Less Code: Slim templates are typically 30-40% shorter than their ERB equivalents. This means faster writing and easier scanning.

Clean and Readable: Once you learn the syntax, Slim templates are remarkably easy to read. The indentation-based structure naturally enforces good organization.

Fast Performance: Slim compiles to Ruby code that’s often faster than ERB, though the difference is negligible in most applications.

Enforces Good Structure: The indentation requirement prevents messy, unstructured code. You can’t create a Slim template that doesn’t follow proper nesting.

Cons

Learning Curve: Team members need to learn new syntax. The first week will involve frequent reference to documentation.

Indentation Sensitivity: Like Python, Slim uses significant whitespace. A misplaced space or tab can break your template, which can be frustrating when debugging.

Less Common: Fewer developers know Slim compared to ERB. Hiring and onboarding may take slightly longer.

Limited Ecosystem Examples: While most gems work fine with Slim, documentation and examples are usually in ERB, requiring mental translation.

When to Use Slim

Slim shines in applications with complex views where you want to maximize readability and minimize boilerplate. It’s perfect for teams that value developer experience and are willing to invest a small amount of time upfront to learn the syntax. If you find yourself frustrated by ERB’s verbosity, Slim is your answer.

Haml

What is it? Haml (HTML Abstraction Markup Language) was one of the first popular alternatives to ERB. It uses indentation to represent HTML structure and eliminates closing tags.

Syntax Example

.user-profile
  %h1= @user.name
  - if @user.admin?
    %span.badge Admin
  %ul.posts
    - @user.posts.each do |post|
      %li= link_to post.title, post_path(post)
Code language: JavaScript (javascript)

Pros

Mature and Stable: Haml has been around since 2006. It’s battle-tested and reliable with excellent documentation.

Cleaner Than ERB: Like Slim, Haml eliminates closing tags and reduces boilerplate significantly.

Good Ecosystem Support: Many gems and libraries explicitly support Haml, and you’ll find plenty of examples and resources online.

Enforces Structure: The indentation requirement keeps your code organized and prevents deeply nested chaos.

Cons

Slower Than Slim: Haml is noticeably slower than Slim in benchmarks, though for most applications this won’t matter.

More Verbose Than Slim: The % prefix for tags makes Haml slightly more verbose than Slim’s minimalist approach.

Indentation Sensitivity: Like Slim, whitespace matters. Mixing tabs and spaces will cause problems.

Feeling Dated: While still widely used, Haml hasn’t evolved as quickly as Slim. It lacks some of the refinements that make Slim feel more modern.

When to Use Haml

Choose Haml if you want an alternative to ERB but prefer a more established option with extensive community support. It’s a safe middle ground between ERB’s verbosity and Slim’s minimalism. Haml is particularly good if you’re maintaining a legacy codebase that already uses it.

Phlex

What is it? Phlex represents a radical departure from traditional templating. Instead of mixing Ruby with HTML-like syntax, Phlex uses pure Ruby classes to build views. It’s component-oriented and type-safe.

Syntax Example

class UserProfile < Phlex::HTML
  def initialize(user)
    @user = user
  end

  def template
    div(class: "user-profile") do
      h1 { @user.name }
      span(class: "badge") { "Admin" } if @user.admin?
      ul(class: "posts") do
        @user.posts.each do |post|
          li { a(href: post_path(post)) { post.title } }
        end
      end
    end
  end
end
Code language: HTML, XML (xml)

Pros

Pure Ruby: No context switching between Ruby and template syntax. Your entire view is just Ruby code, which means better IDE support, easier refactoring, and familiar debugging.

Component Architecture: Phlex encourages building reusable components, leading to better code organization and DRY principles.

Type Safety: Because it’s pure Ruby, you can use tools like Sorbet or RBS for type checking your views.

Excellent Performance: Phlex is extremely fast, often outperforming other template engines significantly.

Testable: Components are just Ruby classes, making them easy to unit test without rendering overhead.

No Markup Parsing: Since there’s no template syntax to parse, there’s one less layer of complexity in your stack.

Cons

Paradigm Shift: Phlex requires a completely different way of thinking about views. This isn’t just new syntax—it’s a new architecture.

Verbose for Simple Views: For basic templates, Phlex can feel like overkill. Writing div { h1 { "Hello" } } instead of <div><h1>Hello</h1></div> doesn’t feel like progress for simple cases.

Limited Ecosystem: Phlex is new. There are fewer examples, fewer ready-made components, and a smaller community.

No Designer-Friendly Workflow: Because Phlex is pure Ruby, front-end developers or designers who aren’t comfortable with Ruby will struggle to contribute to views.

Steep Learning Curve: Understanding how to structure Phlex components well takes time and experience.

When to Use Phlex

Phlex is ideal for component-heavy applications where you want maximum reusability and testability. It’s perfect for design systems, UI libraries, or applications with complex, interactive interfaces. Choose Phlex if your team is comfortable with Ruby and values type safety and performance. It’s also excellent for API-driven applications where you’re building JSON responses rather than full HTML pages.

The Comparison Matrix

Let me break down how these engines stack up across key criteria:

Performance

Winner: Phlex Phlex is the fastest, followed closely by Slim. Haml is slower, and ERB sits in the middle. However, for most applications, template rendering isn’t the bottleneck—database queries and business logic are.

Readability

Winner: Slim Once learned, Slim offers the best balance of conciseness and clarity. ERB is readable but verbose. Haml is good but slightly cluttered with % symbols. Phlex requires Ruby fluency to read comfortably.

Learning Curve

Winner: ERB ERB has virtually no learning curve. Slim and Haml require a day or two to feel comfortable. Phlex requires rethinking your entire approach to views.

Ecosystem Support

Winner: ERB ERB is universal. Everything supports it. Slim and Haml have good support but sometimes require translation. Phlex is still building its ecosystem.

Maintainability

Winner: Phlex/Slim Phlex’s component architecture and Slim’s enforced structure both lead to highly maintainable codebases. ERB’s flexibility can become a maintainability liability. Haml sits in the middle.

Team Onboarding

Winner: ERB Any Rails developer can contribute to ERB templates immediately. The alternatives require training time.

My Recommendations

After years of using all these engines in production, here’s what I recommend:

For New Projects with Small Teams

Use Slim. You’ll write less code, maintain cleaner views, and the learning investment pays off quickly. The performance gains are nice, but the real benefit is how much easier it is to scan and understand Slim templates.

For Large Teams or Open Source

Stick with ERB. The universal knowledge and zero onboarding friction outweigh the benefits of alternatives. Don’t underestimate the value of every Rails developer being able to contribute immediately.

For Component-Heavy Applications

Choose Phlex. If you’re building a complex UI with lots of reusable components, Phlex’s architecture will save you time in the long run. The learning curve is worth it for applications where component composition is central.

For Existing Projects

Don’t Rewrite. If your project already uses Haml or Slim, keep using it. If it uses ERB and you’re happy with it, don’t change. The cost of conversion rarely justifies the benefits.

For Learning

Start with ERB, then try Slim. Master Rails with its default templating engine first. Once you’re comfortable, experiment with Slim on a side project. After you understand the tradeoffs, you’ll be equipped to make informed decisions.

Mixing Engines

Here’s something many developers don’t realize: you can use multiple templating engines in the same Rails application. You might use ERB for most views but Phlex for a complex component or Slim for your admin interface.

This flexibility means you’re not locked into one choice forever. Start with ERB and migrate specific areas to alternatives as needs arise.

The Future

The Rails templating landscape is evolving. Phlex represents a new wave of thinking about views as components rather than templates. Meanwhile, tools like ViewComponent bridge the gap between traditional templates and component architecture.

My prediction? We’ll see more hybrid approaches where simple CRUD views use traditional templates while complex UIs leverage component-based systems like Phlex.

Conclusion

There’s no universally correct answer to “which templating engine should I use?” The right choice depends on your team, your project, and your priorities.

  • ERB for maximum compatibility and zero friction
  • Slim for optimal developer experience and clean code
  • Haml for a mature alternative with good ecosystem support
  • Phlex for component-driven architecture and maximum performance

My personal preference? I use Slim for most projects. The productivity boost is real, the syntax becomes second nature quickly, and I appreciate how it naturally encourages better code organization. But I’ve shipped successful applications with all four engines, and I wouldn’t hesitate to use any of them given the right context.

What matters most isn’t which engine you choose, but that you use it consistently and well. A well-structured ERB codebase beats a messy Slim project every time.

What’s your experience with Rails templating engines? Have you tried alternatives to ERB? I’d love to hear your thoughts in the comments below.


Want to dive deeper into Rails development? Subscribe to my newsletter for weekly tips and insights on building better Rails applications.

Why AI Startups Should Choose Rails Over Python

AI startups often fail due to challenges in supporting layers and product development rather than model quality. Rails offers a fast and structured path for founders to build scalable applications, integrating seamlessly with AI services. While Python excels in research, Rails is favored for production, facilitating swift feature implementation and reliable infrastructure.

TLDR;

Most AI startups fail because they cannot ship a product
not because the model is not good enough
Rails gives founders the fastest path from idea to revenue
Python is still essential for research but Rails wins when the goal is to build a business.

The Real Challenge in AI Today

People love talking about models
benchmarks
training runs
tokens
context windows
all the shiny parts

But none of this is why AI startups fail

Startups fail because the supporting layers around the model are too slow to build:

  • Onboarding systems
  • Billing and subscription logic
  • Admin dashboards
  • User management
  • Customer support tools
  • Background processing
  • Iterating on new features
  • Fixing bugs
  • Maintaining stability

The model is never the bottleneck
The product is
This is exactly where Rails becomes your unfair advantage

Why Rails Gives AI Startups Real Speed

Rails focuses on shipping
It gives you a complete system on day one
The framework removes most of the decisions that slow down small teams
Instead of assembling ten libraries you just start building

The result is simple
A solo founder or a tiny team can move with the speed of a full engineering department,
Everything feels predictable,
Everything fits together,
Everything works the moment you touch it.

Python gives you freedom,
Rails gives you momentum,
Momentum is what gets a startup off the ground.

Rails and AI Work Together Better Than Most People Think

There is a common myth that AI means Python
Only partially true
Python is the best language for training and experimenting
But the moment you are building a feature for real users you need a framework that is designed for production

Rails integrates easily with every useful AI service:

  • OpenAI
  • Anthropic
  • Perplexity
  • Groq
  • Nvidia
  • Mistral
  • Any vector database
  • Any embedding store

Rails makes AI orchestration simple
Sidekiq handles background jobs
Active Job gives structure
Streaming responses work naturally
You can build an AI agent inside a Rails app without hacking your way through a forest of scripts

The truth is that you do not need Python to run AI in production
You only need Python if you plan to become a research lab
Most founders never will

Rails Forces You to Think in Systems

AI projects built in Python often turn into a stack of disconnected scripts
One script imports the next
Another script cleans up data
Another runs an embedding job
This continues until even the founder has no idea what the system actually does

Rails solves this by design
It introduces structure: Controllers, Services, Models, Jobs, Events
It forces you to think in terms of a real application rather than a set of experiments

This shift is a superpower for founders
AI is moving from research to production
Production demands structure
Rails gives you structure without slowing you down

Why Solo Founders Thrive With Rails

When you are building alone you cannot afford chaos
You need to create a system that your future self can maintain
Rails gives you everything that normally requires a team

You can add authentication in a few minutes
You can build a clean admin interface in a single afternoon
You can create background workflows without debugging weird timeouts
You can send emails without configuring a jungle of libraries
You can go from idea to working feature in the same day

This is what every founder actually needs
Not experiments
Not scripts
A product that feels real
A product you can ship this week
A product you can charge money for

Rails gives you that reality


Real Companies That Prove Rails Is Still a Winning Choice

Rails is not a nostalgia framework
It is the foundation behind some of the biggest products ever created

GitHub started with Rails
Shopify grew massive on Rails
Airbnb used Rails in its early explosive phase
Hulu
Zendesk
Basecamp
Dribbble
All Rails

Modern AI driven companies also use Rails in production
Shopify uses it to power AI commerce features
Intercom uses it to support AI customer support workflows
GitHub still relies on Rails for internal systems even as it builds Copilot
Stripe uses Rails for internal tools because the Python stack is too slow for building complex dashboards

These are not lightweight toy projects
These are serious companies that trust Rails because it just works

What You Gain When You Choose Rails

The biggest advantage is development speed
Not theoretical speed
Real speed
The kind that lets you finish an entire feature before dinner

Second
You escape the burden of endless decisions
The framework already gives you the right defaults
You do not waste time choosing from twenty possible libraries for each part of the system

Third
Rails was built for production
This matters more than people admit
You get caching, background jobs, templates, email, tests, routing, security, all included, all consistent, all reliable

Fourth
Rails fits perfectly with modern AI infrastructure: Vector stores, embedding workflows, agent orchestration, streaming responses. It works out of the box with almost no friction

This combination is rare
Rails gives you speed and stability at the same time
Most frameworks give you one or the other
Rails gives you both

Where Rails Is Not the Best Too

There are honest limits. If you are training models working with massive research datasets, writing CUDA kernels or doing deep ML research Python remains the right choice.

If you come from Python the Rails conventions can feel magical or strange at first You might wonder why things happen automatically. But the conventions are there to help you move faster

Hiring can be more challenging in certain regions
There are fewer Rails developers
but the ones you find are usually very strong
and often much more experienced in building actual products

You might also deal with some bias. A few people still assume Rails is old
These people are usually too young to remember that Rails built half the modern internet

The One Thing Every Founder Must Understand

The future of AI will not be won by better models. Models are quickly becoming a commodity. The real victory will go to the teams that build the best products around the models:

  • Onboarding
  • UX
  • speed
  • reliability
  • iteration
  • support
  • support tools
  • customer insights
  • monetization
  • all the invisible details that turn a clever idea into a real business

Rails is the best framework in the world for building these supporting layers fast. This is why it remains one of the most effective choices for early stage AI startups

Use Python for research
Use Rails to build the business
This is the strategy that gives you the highest chance of reaching customers
and more importantly
the highest chance of winning

The Two Hardest Problems in Software Development: Naming Things & Cache Invalidation

The post discusses the common struggles developers face with naming conventions and cache invalidation, humorously portraying them as universal challenges irrespective of experience or technology. It emphasizes that while AI and Ruby tools assist in these areas, the inherent complexities require human reasoning. Ultimately, these issues highlight the uniquely human aspects of software development.

A joke. A reality. A shared developer trauma now with AI and Ruby flavor.

Every industry has its running jokes. Lawyers have billable hours. Doctors have unreadable handwriting. Accountants battle ancient spreadsheets.

Developers?
We have two immortal bosses at the end of every level:

1. Naming things
2. Cache invalidation

These aren’t just memes.
They’re universal rites of passage, the kind of problems that don’t care about your stack, your years of experience, or your productivity plans for the day.

And as modern as our tools get, these two battles remain undefeated.


1. Naming Things: A Daily Existential Crisis

You would think building multi-region distributed systems or designing production-grade blockchains is harder than deciding how to name a method.

But no.
Naming is where confidence goes to die.

There’s something profoundly humbling about:

  • Typing data, deleting it,
  • typing result, deleting it,
  • typing payload, staring at it, deleting it again,
  • then settling on something like final_sanitized_output and hoping future-you understands the intention.

Naming = Thinking

A name isn’t just a word.
It’s a miniature problem statement.

A good name answers:

  • What is this?
  • Why does it exist?
  • What is it supposed to do?
  • Is it allowed to change?
  • Should anyone else touch it?

A bad name answers none of that but invites everyone on your team to ping you on Slack at 22:00 asking “hey what does temp2 mean?”

Not being a native English speaker? Welcome to the Hard Mode DLC

For those of us whose brain grew up in Croatian or Slovenian, naming in English is a special kind of fun.

You might know exactly what you want to say in your own language, but English gives you:

  • three misleading synonyms,
  • one obscure word nobody uses,
  • and a fourth option that feels right but actually means “a small marine snail.”

Sometimes you choose a word that sounds good.
Later a native speaker reviews it and politely suggests:
“Did you mean something else?”

Yes.
I meant something else.
I just didn’t know the word.

And every developer from Europe, Asia, or South America collectively understands this pain.


2. Cache Invalidation: “Why is my app showing data from last week?”

Caching seems easy on paper:

Save expensive data → Serve it fast → Refresh it when needed.

Unfortunately, “when needed” is where the nightmares begin.

Cache invalidation is unpredictable because it lives at the intersection of:

  • time
  • state
  • concurrency
  • user behavior
  • background jobs
  • frameworks
  • deployment pipelines
  • the moon cycle
  • your personal sins

You delete the key.
The stale value still appears.
You restart the server.
It refuses to die.

You clear your browser cache.
Nothing changes.

Then you realize:
Ah. It’s Cloudflare.
Or Redis.
Or Rails fragment caching.
Or your CDN.
Or… you know what, it doesn’t matter anymore. You’re in too deep.


3. “But can’t AI fix it?”

Not… really.
Not even close.

Large language models can:

  • produce suggestions,
  • generate name variations,
  • summarize logic,
  • help brainstorm alternative wording.

But they don’t actually understand your domain, your codebase, your long-term architecture, or your internal conventions.

Their naming suggestions are based on statistical patterns in text not on:

  • business logic
  • your future plans
  • subtle behavior differences
  • what will still make sense in six months
  • what your teammates expect
  • what your product owner actually wants

AI might suggest a “good enough” name that reads nicely,
but it won’t know that half your system expects a value to mean something slightly different, or that “order” conflicts with another concept named “Order” in a separate context.

And with cache invalidation?
AI can generate explanations but it can’t magically deduce your system’s lifetime, caching layers, or deployment quirks.
It cannot predict race conditions or magically detect all the hidden layers where stale data might be hiding like a gremlin.

AI helps you write code faster.
But it does not remove the need for deep understanding, consistent thinking, and human judgment.


4. How Ruby Tries to Save Us From Ourselves

Ruby and Ruby on Rails in particular has spent two decades trying to soften the blow of both naming and system complexity.

Not by solving the problem completely, but by making the playground safer.

Ruby’s Naming Conventions = Guardrails for Humans

Ruby tries to push developers toward sanity through:

  • clear method naming idioms (predicate?, bang!, _before_type_cast)
  • consistent pluralization rules
  • convention-driven file names and classes
  • ActiveRecord naming patterns like Userusers, Personpeople

Rails developers don’t choose how to name directories, controllers, helpers, or models.
Rails chooses for you.

This is not a limitation it’s freedom.

The fewer decisions you have to make about structure, the more mental energy you save for meaningful names, not framework boilerplate.

Ruby Reduces the “Naming Chaos Budget”

Thanks to convention-over-configuration:

  • folders behave predictably
  • classes match filenames
  • methods follow community patterns
  • model names map directly to database entities
  • you don’t spend half your day wondering where things live

Ruby doesn’t fix naming.
It simply reduces the size of the battlefield.

Ruby Also Softens Caching Pain… a Bit

Rails gives you:

  • fragment caching
  • Russian doll caching
  • cache keys with versioning (cache_key_with_version)
  • automatic key invalidation via ActiveRecord touch
  • expiry helpers (expires_in, expires_at)
  • per-request cache stores
  • caching tied to view rendering

Rails tries to help you avoid stale data by structuring caching around data freshness instead of low-level keys.

But even then…

The moment you have:

  • multiple services,
  • background jobs,
  • external APIs,
  • or anything distributed…

Ruby smiles kindly and whispers:
“You’re on your own now, my friend.”


Why These Problems Never Disappear

Because they aren’t technical problems.
They are human constraints on top of technical systems.

  • Naming requires clarity of thinking.
  • Caching requires clarity of system behavior.
  • AI can assist, but it cannot replace understanding.
  • Ruby can guide you, but it cannot decide for you.

The tools help.
The frameworks help.
AI helps.

But at the end of the day, the two hardest problems remain hard because they require the one thing no machine or framework can automate:

your own reasoning.


Final Thoughts

Some days you ship incredible features.
Some days you wage war against a variable name.
Some days you fight stale cached data for three hours before realizing the problem was a CDN rule from 2018.

And every developer, everywhere on Earth, understands these moments.

Naming things and cache invalidation aren’t just computer science problems.
They’re reminders of why software development is deeply human full of ambiguity, creativity, and shared misery.

But honestly?

That’s what keeps it fun.

Saving Money With Embeddings in AI Memory Systems: Why Ruby on Rails is Perfect for LangChain

In the exploration of AI memory systems and embeddings, the author highlights the hidden costs in AI development, emphasizing token management. Leveraging Ruby on Rails streamlines the integration of LangChain for efficient memory handling. Adopting strategies like summarization and selective retrieval significantly reduces expenses, while maintaining readability and scalability in system design.

Over the last few months of rebuilding my Rails muscle memory, I’ve been diving deep into AI memory systems and experimenting with embeddings. One of the biggest lessons I’ve learned is that the cost of building AI isn’t just in the model it’s in how you use it. Tokens, storage, retrieval these are the hidden levers that determine whether your AI stack remains elegant or becomes a runaway expense.

And here’s the good news: with Ruby on Rails, managing these complexities becomes remarkably simple. Rails has always been about turning complicated things into something intuitive and maintainable and when you pair it with LangChain, it feels like magic.


Understanding the Cost of Embeddings

Most people think that running large language models is expensive because of the model itself. That’s only partially true. In practice, the real costs come from:

  • Storing too much raw content: Every extra paragraph you embed costs more in tokens, both for the embedding itself and for later retrieval.
  • Embedding long texts instead of summaries: LLMs don’t need the full novel they often just need the distilled version. Summaries are shorter, cheaper, and surprisingly effective.
  • Retrieving too many memories: Pulling 50 memories for a simple question can cost more than the model call itself. Smart retrieval strategies can drastically cut costs.
  • Feeding oversized prompts into the model: Every extra token in your prompt adds up. Cleaner prompts = cheaper calls.

I’ve seen projects where embedding every word of a document seemed “safe,” only to realize months later that the token bills were astronomical. That’s when I started thinking in terms of summary-first embeddings.


How Ruby on Rails Makes It Easy

Rails is my natural playground for building systems that scale reliably without over-engineering. Why does Rails pair so well with AI memory systems and LangChain? Several reasons:

Migrations Are Elegant
With Rails, adding a vector column with PgVector feels like any other migration. You can define your tables, indexes, and limits in one concise block:

 class AddMemoriesTable < ActiveRecord::Migration[7.1] 
   def change 
     enable_extension "vector" 
     create_table :memories do |t| 
       t.text :content, null: false 
       t.vector :embedding, limit: 1536 
       t.jsonb :metadata 
       t.timestamps 
     end 
   end 
end 


There’s no need for complicated schema scripts. Rails handles the boring but essential details for you.

ActiveRecord Makes Embedding Storage a Breeze
Storing embeddings in Rails is almost poetic. With a simple model, you can create a memory with content, an embedding, and metadata in a single call:

Memory.create!(
  content: "User prefers Japanese and Mexican cuisine.", 
  embedding: embedding_vector,
  metadata: { type: :preference, user_id: 42 }
)Code language: CSS (css)

And yes, you can query those memories by similarity in a single, readable line:

Memory.order(Arel.sql("embedding <=> '[#{query_embedding.join(',')}]'")).limit(5)Code language: HTML, XML (xml)

Rails keeps your code readable and maintainable while you handle sophisticated vector queries.

LangChain Integration is Natural
LangChain is all about chaining LLM calls, memory storage, and retrieval. In Rails, you already have everything you need: models, services, and job queues. You can plug LangChain into your Rails services to:


Saving Money with Smart Embeddings

Here’s the approach I’ve refined over multiple projects:

  1. Summarize Before You Embed
    Instead of embedding full documents, feed the model a summary. A 50-word summary costs fewer tokens but preserves the semantic meaning needed for retrieval.
  2. Limit Memory Retrieval
    You rarely need more than 5–10 memories for a single model call. More often than not, extra memories just bloat your prompt and inflate costs.
  3. Use Metadata Wisely
    Store small, structured metadata alongside your embeddings to filter memories before similarity search. For example, filter by user_id or type instead of pulling all records into the model.
  4. Cache Strategically
    Don’t re-embed unchanged content. Use Rails validations, background jobs, and services to embed only when necessary.

When you combine these strategies, the savings are significant. In some projects, embedding costs dropped by over 70% without losing retrieval accuracy.


Why I Stick With Rails and PostgreSQL

There are many ways to build AI memory systems. You could go with specialized databases, microservices, or cloud vector stores. But here’s what keeps me on Rails and Postgres:

  • Reliability: Postgres is mature, stable, and production-ready. PgVector adds vector search without changing the foundation.
  • Scalability: Rails scales surprisingly well when you keep queries efficient and leverage background jobs.
  • Developer Happiness: Rails lets me iterate quickly. I can prototype, test, and deploy AI memory features without feeling like I’m juggling ten different systems.
  • Future-Proofing: Rails projects can last years without a complete rewrite. AI infrastructure is still evolving having a stable base matters.

Closing Thoughts

AI memory doesn’t have to be complicated or expensive. By thinking carefully about embeddings, summaries, retrieval, and token usage and by leveraging Rails with LangChain you can build memory systems that are elegant, fast, and cost-effective.

For me, Rails is more than a framework. It’s a philosophy: build systems that scale naturally, make code readable, and keep complexity under control. Add PgVector and LangChain to that mix, and suddenly AI memory feels like something you can build without compromise.

In the world of AI, where complexity grows faster than budgets, that kind of simplicity is priceless.

Returning to the Rails World: What’s New and Exciting in Rails 8 and Ruby 3.3+

It’s 2025, and coming back to Ruby on Rails feels like stepping into a familiar city only to find new skyscrapers, electric trams, and an upgraded skyline.
The framework that once defined web development simplicity has reinvented itself once again.

If you’ve been away for a couple of years, you might remember Rails 6 or early Rails 7 as elegant but slightly “classic.”
Fast-forward to today: Rails 8 and Ruby 3.4 together form one of the most modern, high-performance, and full-stack ecosystems in web development.

Let’s explore what changed from Ruby’s evolution to Rails’ latest superpowers.


The Ruby Renaissance: From 3.2 to 3.4

Over the last two years, Ruby has evolved faster than ever.
Performance, concurrency, and developer tooling have all received major love while the language remains as expressive and joyful as ever.

Ruby 3.2 (2023): The Foundation of Modern Ruby

  • YJIT officially production-ready: Introduced a new JIT compiler written in Rust, delivering 20–40% faster execution on Rails apps.
  • Prism Parser (preview): The groundwork for a brand-new parser that improves IDEs, linters, and static analysis.
  • Regexp improvements: More efficient and less memory-hungry pattern matching.
  • Data class proposal: Early syntax experiments to make small, immutable data structures easier to define.

Ruby 3.3 (2024): Performance, Async IO, and Stability

  • YJIT 3.3 update: Added inlining and better method dispatch caching big wins for hot code paths.
  • Fiber Scheduler 2.0: Improved async I/O great for background processing and concurrent network calls.
  • Prism Parser shipped: Officially integrated, paving the way for better tooling and static analysis.
  • Better memory compaction: Long-running apps now leak less and GC pauses are shorter.

Ruby 3.4 (2025): The Next Leap

  • Prism as the default parser making editors and LSPs much more accurate.
  • Official WebAssembly build: You can now compile and run Ruby in browsers or serverless environments.
  • Async and Fibers 3.0: Now tightly integrated into standard libraries like Net::HTTP and OpenURI.
  • YJIT 3.4: Huge startup time and memory improvements for large Rails codebases.
  • Smarter garbage collector: Dynamic tuning for better throughput under load.

Example: Native Async Fetching in Ruby 3.4

require "async"
require "net/http"

Async do
  ["https://rubyonrails.org", "https://ruby-lang.org"].each do |url|
    Async do
      res = Net::HTTP.get(URI(url))
      puts "#{url} → #{res.bytesize} bytes"
    end
  end
end
Code language: PHP (php)

That’s fully concurrent, purely in Ruby no threads, no extra gems.
Ruby has quietly become fast, efficient, and concurrent while keeping its famously clean syntax.


The Rails Revolution: From 7 to 8

While Ruby evolved under the hood, Rails reinvented the developer experience.
Rails 7 introduced the “no-JavaScript-framework” movement with Hotwire.
Rails 8 now expands that vision making real-time, async, and scalable apps easier than ever.

Rails 7 (2022–2024): The Hotwire Era

Rails 7 changed the front-end game:

  • Hotwire (Turbo + Stimulus): Replaced complex SPAs with instant-loading server-rendered apps.
  • Import maps: Let you skip Webpack entirely.
  • Encrypted attributes: encrypts :email became a one-line reality.
  • ActionText and ActionMailbox: Brought full-stack communication features into Rails core.
  • Zeitwerk loader improvements: Faster boot and reloading in dev mode.

Example: Rails 7 Hotwire Simplicity

# app/controllers/messages_controller.rb
def create
  @message = Message.create!(message_params)
  turbo_stream.append "messages", partial: "messages/message", locals: { message: @message }
end
Code language: PHP (php)

That’s a live-updating chat stream with no React, no WebSocket boilerplate.


Rails 8 (2025): Real-Time, Async, and Database-Native

Rails 8 takes everything Rails 7 started and levels it up for the next decade.

Turbo 8 and Turbo Streams 2.0

Hotwire gets more powerful:

  • Streaming updates from background jobs
  • Improved Turbo Frames for nested components
  • Async rendering for faster page loads
class CommentsController < ApplicationController
  def create
    @comment = Comment.create!(comment_params)
    turbo_stream.prepend "comments", partial: "comments/comment", locals: { comment: @comment }
  end
end
Code language: CSS (css)

Now you can push that stream from Active Job or Solid Queue, enabling real-time updates across users.

Solid Queue and Solid Cache

Rails 8 introduces two built-in frameworks that change production infrastructure forever:

  • Solid Queue: Database-backed job queue think Sidekiq performance without Redis.
  • Solid Cache: Native caching framework that integrates with Active Record and scales horizontally.
# Example: background email job using Solid Queue
class UserMailerJob < ApplicationJob
  queue_as :mailers

  def perform(user_id)
    UserMailer.welcome_email(User.find(user_id)).deliver_now
  end
end
Code language: CSS (css)

No Redis, no extra service everything just works out of the box.

Async Queries and Connection Pooling

Rails 8 adds native async database queries and automatic connection throttling for multi-threaded environments.
This pairs perfectly with Ruby’s improved Fiber Scheduler.

users = ActiveRecord::Base.async_query do
  User.where(active: true).to_a
end
Code language: PHP (php)

Smarter Defaults, Stronger Security

  • Active Record Encryption expanded with deterministic modes
  • Improved CSP and SameSite protections
  • Rails generators now use more secure defaults for APIs and credentials

Developer Experience: Rails Feels Modern Again

The latest versions of Rails and Ruby have also focused heavily on DX (developer experience).

  • bin/rails console --sandbox rolls back all changes automatically.
  • New error pages with interactive debugging.
  • ESBuild 3 & Bun support for lightning-fast JS builds.
  • Improved test parallelization with async jobs and Capybara integration.
  • ViewComponent and Hotwire integration right from generators.

Rails in 2025 feels sleek, intelligent, and incredibly cohesive.


The Future of Rails and Ruby Together

With Ruby 3.4’s concurrency and Rails 8’s async, streaming, and caching power, Rails has evolved into a true full-stack powerhouse again capable of competing with modern Node, Elixir, or Go frameworks while staying true to its elegant roots.

It’s not nostalgia it’s progress built on the foundation of simplicity.

If you left the Rails world thinking it was old-fashioned, this is your invitation back.
You’ll find your favorite framework faster, safer, and more capable than ever before.


Posted by Ivan Turkovic
Rubyist, software engineer, and believer in beautiful code.

AngularJS and Ruby on Rails work together

Finding the best integration of AngularJS and Ruby on Rails

Recently I got really excited with AngularJS so to make it work perfectly with Ruby on Rails there are some configurations needed. There are available blog posts on how to integrate it perfectly but somehow I wasn’t happy with the available preferences. Some offered to add the javascript files manually to the project and to manual project organization, others offered some kind of gem packager or even automated as a rails app template.

My goal is to describe how to start new rails app project from the scratch but the instructions should be succinct enough to be able to reuse them for the existing project (Actually I did extract it from the existing application I am working on). For the front end development I recently discovered great gem that really can make it more closer to pure full stack javascript development.
Bower is a great javascript package manager by the people who are working on Twitter Boostrap. It is like Bundler but made for javascript instead of ruby language. When I found out that there is a ruby gem that is integrated with rake tasks so I can easily update all javascript libraries without needing adding gem library for each one. The gem is called bower-rails.

My initial plan is to evolve this post into a series of blog posts on how to develop a fully functional demo application so I’ve included some steps that are might not needed but are good to have. Don’t worry I will provide explanation why I am using each of them.
Here is my plan what I will try to achieve with this series of posts:

  • creating a new demo project with angularJS from scratch, showing all my changes along the way, and trying to explain every step. this will include creating basic rails 4 app
  • adding basic gems
  • setup front end development with Bower
  • adding angularJS
  • implementing basic Rails and AngularJS controllers

Continue reading “AngularJS and Ruby on Rails work together”