Skip to content

Signal Through the Noise

Honest takes on code, AI, and what actually works

Menu
  • Home
  • My Story
  • Experience
  • Services
  • Contacts
Menu

What Makes Ruby Different: Unique Structures vs Python, Java, JavaScript

Posted on January 10, 2026January 8, 2026 by ivan.turkovic

In my previous post on Ruby’s building blocks, I covered when to use Struct, Data, Class, and Module. But I glossed over something important: many of these constructs don’t exist in other languages – or exist in such diminished forms that they barely count.

Ruby isn’t just another object-oriented language with different syntax. It has genuinely unique structures that change how you think about code. If you’re coming from Python, Java, JavaScript, or C#, these are the things that will make you say “wait, you can do that?”

This isn’t about Ruby being “better” in some abstract sense. It’s about understanding what Ruby offers that others don’t – and why those differences matter for writing expressive, maintainable code.


Struct: Class Generation as a First-Class Feature

Most languages make you write classes by hand. Ruby lets you generate them.

Person = Struct.new(:name, :email, :age)Code language: PHP (php)

That single line creates a complete class with:

  • A constructor accepting three arguments
  • Getter and setter methods for each attribute
  • Equality comparison based on values
  • A sensible to_s representation
  • Hash and array-like access (person[:name], person[0])

What Other Languages Offer

Python has namedtuple and @dataclass:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    email: str
    age: intCode language: CSS (css)

This is close, but it’s a decorator that modifies a class you still have to define. You need type annotations (even if you don’t want them). And it was added in Python 3.7 – Struct has been in Ruby since the beginning.

JavaScript has nothing built-in. You write classes or use plain objects:

class Person {
  constructor(name, email, age) {
    this.name = name;
    this.email = email;
    this.age = age;
  }
}Code language: JavaScript (javascript)

Java added Records in Java 14:

public record Person(String name, String email, int age) {}Code language: JavaScript (javascript)

Clean, but it took until 2020 to arrive, and records are immutable – you can’t choose.

C# has records too (C# 9.0):

public record Person(string Name, string Email, int Age);Code language: PHP (php)

Same story – late addition, immutable by default.

Why Ruby’s Approach Is Better

Ruby’s Struct isn’t just syntax sugar. It’s a class factory – a method that returns a new class. This is possible because Ruby classes are themselves objects.

# Struct.new returns a class
PersonClass = Struct.new(:name, :email)
PersonClass.class  # => Class

# You can subclass it
class Employee < Struct.new(:name, :department)
  def formatted
    "#{name} (#{department})"
  end
end

# You can generate classes dynamically
def create_model(fields)
  Struct.new(*fields) do
    def valid?
      members.none? { |m| send(m).nil? }
    end
  end
end

User = create_model(:id, :name, :email)Code language: HTML, XML (xml)

This meta-programming capability – generating classes at runtime based on data – is natural in Ruby and awkward or impossible in most other languages.


Data: Immutable Value Objects Done Right

Ruby 3.2 introduced Data – like Struct but immutable. This might seem minor until you realize how other languages handle immutability.

Point = Data.define(:x, :y)
origin = Point.new(0, 0)
origin.x = 5  # NoMethodError - no setter existsCode language: PHP (php)

The Problem Data Solves

In most languages, making something immutable requires discipline or verbosity.

Java (pre-records):

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }

    @Override
    public boolean equals(Object o) { /* ... */ }

    @Override
    public int hashCode() { /* ... */ }
}Code language: PHP (php)

That’s 20+ lines for what Ruby does in one.

Python:

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: intCode language: CSS (css)

Better, but frozen=True is opt-in and easy to forget. And “frozen” dataclasses can still contain mutable objects – the immutability is shallow.

JavaScript:

const point = Object.freeze({ x: 0, y: 0 });Code language: JavaScript (javascript)

Object.freeze is shallow, doesn’t prevent reassignment of the variable, and provides no structural guarantees.

Why Ruby’s Data Is Better

Ruby’s Data makes immutability the point of the construct, not an option you enable. When you see Data.define, you know immediately that instances won’t change. It’s semantic clarity through distinct types.

# The type itself communicates intent
Config = Data.define(:api_key, :timeout, :retries)

# Compare to Struct where you have to read the code
# to know if anyone mutates it
Config = Struct.new(:api_key, :timeout, :retries)Code language: PHP (php)

And like Struct, Data is a class factory with full meta-programming support:

Point = Data.define(:x, :y) do
  def distance_from_origin
    Math.sqrt(x**2 + y**2)
  end

  def +(other)
    Point.new(x + other.x, y + other.y)
  end
endCode language: JavaScript (javascript)

Blocks: Code as a Casual Parameter

Blocks are Ruby’s most distinctive feature. They let you pass code to methods as naturally as passing data.

[1, 2, 3].map { |n| n * 2 }

File.open("data.txt") do |file|
  file.each_line { |line| puts line }
end

3.times { puts "Hello" }Code language: JavaScript (javascript)

What Other Languages Offer

Python has comprehensions and lambdas:

[n * 2 for n in [1, 2, 3]]  # Comprehension - limited to specific patterns

list(map(lambda n: n * 2, [1, 2, 3]))  # Lambda - verboseCode language: PHP (php)

Python lambdas are single expressions only. Multi-line logic requires a named function.

JavaScript has arrow functions:

[1, 2, 3].map(n => n * 2)Code language: JavaScript (javascript)

Close to Ruby’s elegance, but JavaScript doesn’t have the yield pattern for implicit blocks.

Java has lambdas (since Java 8):

Arrays.asList(1, 2, 3).stream().map(n -> n * 2).collect(Collectors.toList());Code language: CSS (css)

Functional, but verbose. And you need to explicitly create streams.

C# has LINQ and lambdas:

new[] {1, 2, 3}.Select(n => n * 2);Code language: JavaScript (javascript)

Quite elegant, actually. C# learned from Ruby.

Why Ruby’s Blocks Are Better

Ruby blocks aren’t just anonymous functions – they’re integrated into the language’s control flow. The yield keyword makes blocks feel like native syntax extensions.

def with_timing
  start = Time.now
  result = yield  # Execute the block
  puts "Took #{Time.now - start} seconds"
  result
end

# Use it like a language construct
with_timing do
  expensive_operation
endCode language: PHP (php)

This pattern is awkward in other languages. Python would need decorators or context managers. JavaScript would need callbacks or explicit function calls.

Ruby also distinguishes between blocks, procs, and lambdas – giving you control over how code-as-data behaves:

# Block: implicit, tied to method call
[1, 2, 3].each { |n| puts n }

# Proc: stored code, flexible return behavior
my_proc = Proc.new { |n| n * 2 }

# Lambda: stored code, strict argument checking
my_lambda = ->(n) { n * 2 }Code language: PHP (php)

The ability to capture a block as a proc (&block) and convert between these forms gives you precise control that other languages lack.


Modules: Namespacing and Mixins in One Construct

Most languages separate namespacing from code sharing. Ruby combines them in modules.

module Searchable
  def search(query)
    # Shared search logic
  end
end

class Article
  include Searchable
end

class Product
  include Searchable
endCode language: PHP (php)

What Other Languages Offer

Python uses files as modules (namespacing) and multiple inheritance or mixins (code sharing):

# Namespacing: just use the file/package system
from myapp.utils import helper

# Code sharing: multiple inheritance or mixins
class SearchMixin:
    def search(self, query):
        pass

class Article(Model, SearchMixin):
    pass

Python’s approach works, but conflates namespacing with file structure. And multiple inheritance is famously problematic (diamond problem).

Java has packages (namespacing) and interfaces with default methods (since Java 8):

public interface Searchable {
    default void search(String query) {
        // Default implementation
    }
}

public class Article implements Searchable {
}Code language: PHP (php)

Interfaces with defaults are Java playing catch-up with Ruby’s mixins – and they’re limited because interfaces can’t have state.

JavaScript has ES6 modules (namespacing) and no built-in mixin pattern:

// Namespacing: import/export
import { helper } from './utils';

// Code sharing: typically Object.assign or class extends
const SearchMixin = {
  search(query) { /* ... */ }
};

Object.assign(Article.prototype, SearchMixin);Code language: JavaScript (javascript)

Awkward and doesn’t integrate with the class system.

C# has namespaces and no mixins:

namespace MyApp.Utils { }

// Code sharing requires inheritance or extension methods
// No direct equivalent to mixinsCode language: JavaScript (javascript)

Why Ruby’s Modules Are Better

Ruby modules serve triple duty:

  1. Namespace: Group related code under a name
  2. Mixin: Share behavior across unrelated classes
  3. Singleton container: Hold module-level methods
module Payments
  # Namespace for related classes
  class Processor; end
  class Refund; end

  # Module-level utility
  def self.default_currency
    "USD"
  end
end

module Auditable
  # Mixin for sharing behavior
  def audit_trail
    @audit_trail ||= []
  end

  def record_change(change)
    audit_trail << { change: change, at: Time.now }
  end
end

class Invoice < Payments::Processor
  include Auditable  # Mix in auditing capability
endCode language: CSS (css)

The key insight: include inserts the module into the class’s ancestor chain, making method lookup work naturally. This is cleaner than multiple inheritance because modules don’t carry their own initialization or identity – they’re pure behavior packages.


Open Classes: Extending Anything, Anytime

Ruby classes are never closed. You can add methods to any class – including built-in ones – at any time.

class String
  def palindrome?
    self == self.reverse
  end
end

"radar".palindrome?  # => true
"hello".palindrome?  # => false

What Other Languages Offer

Python allows monkey-patching but discourages it:

def palindrome(self):
    return self == self[::-1]

str.palindrome = palindrome  # Works, but frowned uponCode language: PHP (php)

It’s possible but feels like a hack. Python’s culture strongly discourages this.

JavaScript allows extending prototypes:

String.prototype.palindrome = function() {
  return this === this.split('').reverse().join('');
};Code language: JavaScript (javascript)

Possible, but universally considered bad practice in JavaScript.

Java and C#: Not possible. Classes are closed after compilation.

Why Ruby’s Open Classes Are Better

Ruby doesn’t just permit open classes – it embraces them. The ecosystem is built around this capability.

ActiveSupport extends core classes tastefully:

# From ActiveSupport
1.day.ago
"hello_world".camelize
[1, 2, 3].sumCode language: CSS (css)

Refinements provide scoped modifications (since Ruby 2.0):

module StringExtensions
  refine String do
    def palindrome?
      self == reverse
    end
  end
end

class MyClass
  using StringExtensions  # Only active in this scope

  def check(word)
    word.palindrome?
  end
end

"radar".palindrome?  # NoMethodError - refinement not active here

This gives you the power of open classes with the safety of scoped changes.


method_missing: The Ultimate Flexibility

When you call a method that doesn’t exist, Ruby doesn’t just fail – it gives you a chance to handle it.

class Flexible
  def method_missing(name, *args)
    puts "You called #{name} with #{args.inspect}"
  end
end

obj = Flexible.new
obj.anything_at_all(1, 2, 3)
# => "You called anything_at_all with [1, 2, 3]"

What Other Languages Offer

Python has __getattr__:

class Flexible:
    def __getattr__(self, name):
        def method(*args):
            print(f"You called {name} with {args}")
        return method

Similar capability, but the syntax is clunkier.

JavaScript has Proxy (ES6):

const handler = {
  get(target, prop) {
    return (...args) => console.log(`Called ${prop} with`, args);
  }
};

const obj = new Proxy({}, handler);
obj.anything(1, 2, 3);Code language: JavaScript (javascript)

Powerful but verbose and not commonly used.

Java and C#: Not possible without reflection hacks that don’t integrate with normal method calls.

Why Ruby’s method_missing Is Better

Ruby’s method_missing enables some of the language’s most elegant patterns.

Dynamic finders in ActiveRecord:

User.find_by_email("alice@example.com")
User.find_by_email_and_status("alice@example.com", "active")Code language: CSS (css)

These methods don’t exist until you call them – ActiveRecord generates them on the fly.

Builder patterns:

class XMLBuilder
  def initialize
    @xml = ""
  end

  def method_missing(tag, content = nil, &block)
    @xml += "<#{tag}>"
    if block
      @xml += instance_eval(&block)
    else
      @xml += content.to_s
    end
    @xml += ""
  end
end

builder = XMLBuilder.new
builder.html do
  head do
    title "My Page"
  end
  body do
    p "Hello, world!"
  end
endCode language: JavaScript (javascript)

This creates a domain-specific language that reads like the output format itself.


Symbols: Identity Without the Overhead

Symbols look like strings but behave like identifiers. They’re immutable, interned, and compared by identity rather than content.

:name.object_id == :name.object_id  # => true (same object)
"name".object_id == "name".object_id  # => false (different objects)Code language: PHP (php)

What Other Languages Offer

Python has no direct equivalent. You use strings everywhere:

{"name": "Alice", "email": "alice@example.com"}Code language: JSON / JSON with Comments (json)

JavaScript added Symbols in ES6, but they’re used differently:

const name = Symbol('name');
obj[name] = "Alice";  // Private-ish propertyCode language: JavaScript (javascript)

JavaScript Symbols are for hiding properties, not for readable keys.

Java and C#: Use strings or enums. No equivalent concept.

Why Ruby’s Symbols Are Better

Symbols are perfect for identifiers that aren’t data – hash keys, method names, options, states.

# Hash keys (overwhelmingly idiomatic)
user = { name: "Alice", email: "alice@example.com" }

# Options
redirect_to root_path, notice: "Welcome back!"

# States and types
status = :pending
case status
when :pending then handle_pending
when :approved then handle_approved
end

# Method references
users.map(&:name)  # Same as users.map { |u| u.name }Code language: PHP (php)

The distinction between symbols and strings makes intent clearer. When you see a symbol, you know it’s an identifier. When you see a string, you know it’s data that might be displayed or manipulated.


Everything Is an Object (And Everything Is Executable)

In Ruby, there are no primitives. Numbers, booleans, even nil – all objects with methods.

42.times { puts "hello" }
true.to_s  # => "true"
nil.nil?   # => trueCode language: PHP (php)

But it goes further. Classes are objects. Methods are objects. Code blocks can become objects.

# Classes are objects of class Class
String.class  # => Class
String.ancestors  # => [String, Comparable, Object, Kernel, BasicObject]

# Methods can be extracted as objects
method_obj = "hello".method(:upcase)
method_obj.call  # => "HELLO"

# Code becomes objects
my_block = -> { puts "I'm a lambda" }
my_block.call

What Other Languages Offer

Python is close – most things are objects:

(42).__add__(8)  # Works
type(42)  # Code language: PHP (php)

But Python has some non-object constructs (None is not quite as object-like, there are statement-vs-expression distinctions).

JavaScript has object wrappers for primitives, but the distinction exists:

typeof 42  // "number" - primitive
typeof new Number(42)  // "object" - wrapperCode language: JavaScript (javascript)

Java and C# have primitives vs objects, requiring boxing/unboxing.

Why Ruby’s Consistency Is Better

When everything is an object, you don’t have mental mode-switching. The same tools (method calls, introspection, extension) work everywhere.

# Extend integers
class Integer
  def factorial
    (1..self).reduce(1, :*)
  end
end

5.factorial  # => 120

# Introspect anything
42.methods.grep(/div/)  # => [:div, :divmod, :fdiv]

# Pass methods around
[1, 2, 3].map(&1.method(:+))  # => [2, 3, 4]

This consistency reduces cognitive load. You learn one set of patterns and apply them everywhere.


The Eigenclass: Object-Level Customization

Every Ruby object can have its own private class called the eigenclass (or singleton class). This lets you add methods to individual objects.

alice = "Alice"

def alice.shout
  upcase + "!"
end

alice.shout  # => "ALICE!"
"Bob".shout  # NoMethodError - only alice has this methodCode language: PHP (php)

What Other Languages Offer

Python allows adding methods to instances:

alice = "Alice"

def shout(self):
    return self.upper() + "!"

import types
alice.shout = types.MethodType(shout, alice)  # Doesn't work - strings are immutableCode language: PHP (php)

Python can do this with custom classes but not built-in types.

JavaScript can add properties to any object:

const alice = { name: "Alice" };
alice.shout = function() { return this.name.toUpperCase() + "!"; };Code language: JavaScript (javascript)

Works, but there’s no formal concept of “this object’s private class.”

Java and C#: Not possible without creating a subclass.

Why Ruby’s Eigenclass Is Better

The eigenclass makes Ruby’s object model complete. Class methods are actually methods on the eigenclass of the class object:

class User
  def self.count  # This is actually defined on User's eigenclass
    # ...
  end
end

# Equivalent to:
class User
  class << self  # Open the eigenclass
    def count
      # ...
    end
  end
end

This unifies instance methods and class methods under one model – both are just methods, defined on different classes (the regular class vs. the eigenclass).

Understanding the eigenclass explains why include adds instance methods while extend adds class methods – extend is actually including into the eigenclass.


Why This Matters: Expressiveness and Joy

These features aren’t academic curiosities. They enable a style of programming that’s hard to achieve elsewhere:

Domain-Specific Languages that read like natural language:

describe User do
  it "validates email presence" do
    expect(User.new(email: nil)).not_to be_valid
  end
endCode language: PHP (php)

Configuration that’s just Ruby:

Rails.application.configure do
  config.cache_classes = true
  config.eager_load = true
endCode language: JavaScript (javascript)

APIs that feel native:

class Article < ApplicationRecord
  belongs_to :author
  has_many :comments
  validates :title, presence: true
  scope :published, -> { where(published: true) }
endCode language: HTML, XML (xml)

None of this is magic – it’s blocks, method_missing, open classes, and modules working together.


The Trade-Offs

Ruby’s flexibility has costs:

Performance: Dynamic method dispatch is slower than static. Ruby trades speed for expressiveness.

Predictability: When anything can be modified, you can’t always be sure what code will do. Discipline matters.

Tooling: Static analysis is harder when methods might not exist until runtime. IDEs have more trouble helping you.

But these trade-offs are conscious choices. Ruby optimizes for programmer happiness and productivity, trusting developers to use flexibility responsibly.


Conclusion: Different, Not Just Different Syntax

Ruby isn’t just Python with different syntax or Java without types. Its unique structures – Struct as a class factory, blocks with yield, modules as mixins, open classes, method_missing, symbols, the eigenclass – create a fundamentally different programming experience.

These features work together. Blocks enable DSLs. Modules enable clean code sharing. Open classes enable tasteful extensions. method_missing enables dynamic interfaces. The eigenclass unifies the object model.

Understanding these Ruby-specific concepts is key to writing idiomatic Ruby. Don’t just translate patterns from other languages – embrace what makes Ruby different.

That’s where the joy is.


Coming from another language and finding Ruby’s flexibility disorienting? Or have you found Ruby constructs that made you wish other languages had them? I’d love to hear your perspective.

Leave a Reply Cancel reply

You must be logged in to post a comment.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  • Instagram
  • Facebook
  • GitHub
  • LinkedIn

Recent Posts

  • What Makes Ruby Different: Unique Structures vs Python, Java, JavaScript
  • Ruby’s Building Blocks: When to Use What (And Why)
  • A CTO Would Be Bored by Tuesday
  • What I Wrote About in 2025
  • A Christmas Eve Technology Outlook: Ruby on Rails and Web Development in 2026

Recent Comments

  • A CTO Would Be Bored by Tuesday - Signal Through the Noise on Contact Me
  • What I Wrote About in 2025 - Ivan Turkovic on From Intentions to Impact: Your 2025 Strategy Guide (Part 2)
  • From Intentions to Impact: Your 2025 Strategy Guide (Part 2) - Ivan Turkovic on Stop Procrastinating in 2025: Part 1 – Building Your Foundation Before New Year’s Resolutions
  • שמוליק מרואני on Extending Javascript objects with a help of AngularJS extend method
  • thorsson on AngularJS directive multiple element

Archives

  • January 2026
  • December 2025
  • November 2025
  • October 2025
  • September 2025
  • August 2025
  • July 2025
  • May 2025
  • April 2025
  • March 2025
  • January 2021
  • April 2015
  • November 2014
  • October 2014
  • June 2014
  • April 2013
  • March 2013
  • February 2013
  • January 2013
  • April 2012
  • October 2011
  • September 2011
  • June 2011
  • December 2010

Categories

  • AI
  • AngularJS
  • blockchain
  • development
  • ebook
  • Introduction
  • mac os
  • personal
  • personal development
  • presentation
  • productivity
  • ruby
  • ruby on rails
  • sinatra
  • start
  • startup
  • success
  • Uncategorized

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org
© 2026 Signal Through the Noise | Powered by Superbs Personal Blog theme