PONY λ M2 Modula-2

C.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing C and Ruby

C17 (GCC) Ruby 4.0
Hello World & Running
Hello, World
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
puts "Hello, World!"
Ruby programs need no #include directives, no main function, and no return 0. Code at the top level executes immediately, line by line. puts prints a value followed by a newline — the direct equivalent of printf("...\n").
Running a program
// Compile then run: // gcc -o hello hello.c && ./hello // With math: // gcc hello.c -lm -o hello // Check with: // ./hello; echo "exit: $?"
# Run directly (no compile step): # ruby hello.rb # # Run a one-liner: # ruby -e 'puts "Hello!"' # # Interactive REPL: # irb
Ruby is interpreted — there is no compile step, no object file, no linker. ruby hello.rb parses, compiles to bytecode, and executes in one command. For quick experiments, irb (Interactive Ruby) provides a REPL. The -e flag executes a one-liner without creating a file.
Comments
#include <stdio.h> // Single-line comment /* Multi-line comment */ int main(void) { int count = 0; /* inline comment */ printf("%d\n", count); return 0; }
# Single-line comment =begin Multi-line comment =end count = 0 # inline comment puts count
Ruby uses # for single-line comments — one character shorter than //. The =begin / =end block comment exists but is almost never used in practice; consecutive # lines are the idiomatic Ruby style for multi-line comments. Both markers must be at the very beginning of the line.
Output — puts, print, p
#include <stdio.h> int main(void) { printf("Hello\n"); // with newline printf("no newline"); // without newline printf("x = %d\n", 42); // formatted fprintf(stderr, "error\n"); // to stderr return 0; }
puts "Hello" # with newline print "no newline" # without newline puts # blank line puts "x = #{42}" # interpolation p "hello" # inspect: shows "hello" with quotes p [1, 2, 3] # inspect: shows [1, 2, 3]
Ruby has three main output methods: puts adds a newline (like printf("...\n")); print does not; p calls .inspect on its argument, which shows the type and structure — invaluable for debugging. Unlike printf, Ruby uses string interpolation (#{expression}) instead of format specifiers for most output.
Variables & Types
No type declarations
#include <stdio.h> int main(void) { int count = 42; double price = 9.99; char name[] = "Alice"; printf("%d %.2f %s\n", count, price, name); return 0; }
count = 42 price = 9.99 name = "Alice" puts "#{count} #{price} #{name}"
Ruby variables are created on first assignment — no type declaration, no int, no double, no char[]. The runtime tracks types automatically. Variable names use snake_case by convention. There is no distinction between stack-allocated primitives and heap-allocated objects — everything in Ruby is an object on the heap.
Dynamic typing — variables can change type
#include <stdio.h> int main(void) { /* C: type is fixed at declaration */ int value = 42; /* value = "hello"; // compile error */ printf("%d\n", value); return 0; }
# Ruby: a variable can hold any type at any time value = 42 puts value.class # Integer value = "hello" puts value.class # String value = [1, 2, 3] puts value.class # Array
In Ruby, variables are not bound to a type — only objects are. Reassigning a variable to a different type is perfectly valid. The .class method returns the actual runtime class of any object. This is fundamentally different from C, where a variable's type is fixed at declaration and enforced by the compiler.
Constants
#include <stdio.h> #define MAX_RETRIES 3 #define API_URL "https://example.com" int main(void) { printf("%d\n", MAX_RETRIES); printf("%s\n", API_URL); return 0; }
MAX_RETRIES = 3 API_URL = "https://example.com" puts MAX_RETRIES puts API_URL
Ruby constants start with an uppercase letter; SCREAMING_SNAKE_CASE is the convention for simple values. Unlike C's #define macros (which are textual substitution with no scope), Ruby constants are real values stored in the runtime and respect module/class scope. Reassigning a constant produces a warning but is not a fatal error.
nil — not NULL
#include <stdio.h> #include <stddef.h> int main(void) { char *pointer = NULL; if (pointer == NULL) { printf("pointer is null\n"); } /* Dereferencing NULL → segfault */ return 0; }
pointer = nil puts pointer.nil? # true puts pointer.class # NilClass puts pointer.to_s # "" (empty string — no segfault) puts pointer.to_a.inspect # [] (empty array) puts pointer.to_i # 0
Ruby's nil is a real object of class NilClass — it is not an invalid memory address. Unlike C's NULL, dereferencing nil does not segfault; calling a method that nil does not understand raises NoMethodError. Conversion methods like nil.to_s, nil.to_a, and nil.to_i all return sensible empty values.
Truthiness — only nil and false are falsy
#include <stdio.h> int main(void) { /* C: 0, NULL, '' are all falsy */ if (0) printf("zero is truthy\n"); /* no */ if (1) printf("1 is truthy\n"); /* yes */ if (NULL) printf("NULL is truthy\n"); /* no */ return 0; }
# Ruby: ONLY nil and false are falsy puts "0 is truthy" if 0 # prints! puts """ is truthy" if "" # prints! puts "[] is truthy" if [] # prints! puts "nil is falsy" unless nil # prints! puts "false is falsy" unless false # prints!
Ruby's truthiness rules are far simpler than C's: exactly two values are falsy — nil and false. Everything else is truthy, including 0, "", [], and 0.0. This is a common source of bugs when C programmers write Ruby — if count in C checks for non-zero; in Ruby it always succeeds because 0 is truthy.
Multiple assignment / destructuring
#include <stdio.h> int main(void) { int first = 1, second = 2, third = 3; /* swap requires temp variable: */ int temp = first; first = second; second = temp; printf("%d %d\n", first, second); return 0; }
# Assign multiple variables in one line first, second, third = 1, 2, 3 puts "#{first} #{second} #{third}" # Swap without a temp variable first, second = second, first puts "#{first} #{second}" # Destructure an array head, *tail = [10, 20, 30, 40] puts head puts tail.inspect
Ruby supports parallel assignment: the right-hand side is fully evaluated before assignment, enabling a clean swap without a temporary variable. The splat operator * in a destructuring pattern captures remaining elements into an array. This eliminates a whole class of C boilerplate — swapping, returning multiple values, and unpacking results.
Strings
Strings are objects, not char arrays
#include <stdio.h> #include <string.h> int main(void) { char greeting[] = "Hello, World!"; printf("length: %zu\n", strlen(greeting)); printf("upper: "); for (size_t index = 0; index < strlen(greeting); index++) { putchar(greeting[index] >= 'a' && greeting[index] <= 'z' ? greeting[index] - 32 : greeting[index]); } putchar('\n'); return 0; }
greeting = "Hello, World!" puts greeting.length puts greeting.upcase puts greeting.downcase puts greeting.reverse puts greeting.include?("World") puts greeting[0..4]
In Ruby, strings are first-class objects with a rich API. Operations that require #include <string.h> and manual loops in C — like converting case, checking for a substring, or reversing — are single method calls in Ruby. There are no null terminators, no buffer overflows, and no need to track the length separately.
String interpolation
#include <stdio.h> int main(void) { char name[] = "Alice"; int age = 30; char message[64]; snprintf(message, sizeof(message), "Hello, %s! You are %d years old.", name, age); printf("%s\n", message); return 0; }
name = "Alice" age = 30 message = "Hello, #{name}! You are #{age} years old." puts message # Any expression works inside #{} puts "Next year: #{age + 1}" puts "Name length: #{name.length} chars"
Ruby uses #{expression} inside double-quoted strings — a far more concise alternative to sprintf / snprintf. Any Ruby expression is valid inside the braces, including method calls and arithmetic. Single-quoted strings do not interpolate: '#{name}' prints the literal text #{name}. No format specifiers, no buffer size, no null terminator to manage.
Common string methods
#include <stdio.h> #include <string.h> #include <ctype.h> int main(void) { char text[] = " Hello, World! "; /* strip: manual index arithmetic */ /* replace: manual char-by-char scan */ /* split: strtok with side effects */ printf("length: %zu\n", strlen(text)); return 0; }
text = " Hello, World! " puts text.strip puts text.strip.length puts text.strip.gsub("World", "Ruby") puts text.strip.split(", ").inspect puts text.strip.start_with?("Hello") puts text.strip.chars.first(3).inspect
Ruby's String class has over 80 built-in methods. strip trims whitespace from both ends; gsub performs a global substitution; split returns an array. All these operations require manual loops, temporary buffers, or external libraries in C. Ruby strings also support regular expressions natively via match, scan, and gsub with pattern arguments.
Multiline strings (heredoc)
#include <stdio.h> int main(void) { /* C: multi-line strings via adjacent literals */ const char *message = "Dear Alice,\n" "\n" "Welcome to the team!\n" "\n" "Regards,\n" "HR"; printf("%s\n", message); return 0; }
message = <<~LETTER Dear Alice, Welcome to the team! Regards, HR LETTER puts message.chomp
Ruby's squiggly heredoc (<<~) strips leading whitespace automatically, so indentation in the source does not appear in the string. The terminator label is arbitrary. chomp removes the trailing newline that heredocs include. Unlike C's adjacent string literal trick, heredocs support interpolation by default — use <<~'HEREDOC' (single-quoted terminator) to disable it.
String formatting with %
#include <stdio.h> int main(void) { printf("%-10s: %6.2f\n", "Price", 9.99); printf("Hex: 0x%08X\n", 255); printf("Count: %05d\n", 42); return 0; }
puts "%-10s: %6.2f" % ["Price", 9.99] puts "Hex: 0x%08X" % 255 puts "Count: %05d" % 42
Ruby's % operator applies printf-style format strings — the same specifiers as C's printf, but without #include <stdio.h> and with a cleaner call syntax. For multiple arguments, pass an array: "format" % [arg1, arg2]. sprintf and its alias format are also available. For most output, string interpolation is preferred.
Frozen strings (Ruby 4.0)
#include <stdio.h> #include <string.h> int main(void) { /* C string literals are in read-only memory: */ const char *read_only = "hello"; /* read_only[0] = 'H'; // undefined behavior */ char mutable_buf[] = "hello"; /* copy on stack */ mutable_buf[0] = 'H'; printf("%s\n", mutable_buf); return 0; }
# In Ruby 4.0, string literals are frozen (immutable) by default greeting = "hello" puts greeting.frozen? # true begin greeting << ", world" # attempt to mutate rescue FrozenError => error puts error.message end mutable = greeting.dup # dup creates an unfrozen copy mutable << ", world" puts mutable
Ruby 4.0 freezes all string literals by default, analogous to C's const char * literals residing in read-only memory. Attempting to mutate a frozen string raises FrozenError instead of causing undefined behavior. Use dup to get a mutable copy, or use the unary plus operator: +greeting returns an unfrozen duplicate.
Numbers
Arbitrary precision integers
#include <stdio.h> #include <stdint.h> int main(void) { /* C: fixed-width types; overflow wraps silently */ uint64_t max_u64 = UINT64_MAX; printf("%llu\n", (unsigned long long)max_u64); printf("%llu\n", (unsigned long long)(max_u64 + 1)); /* wraps to 0 */ return 0; }
max_u64 = 2**64 - 1 puts max_u64 puts max_u64.class # Integer puts (max_u64 + 1) # no overflow — arbitrary precision puts (max_u64 * max_u64) # just works
Ruby has a single Integer type that handles arbitrarily large numbers — no int, long, uint64_t, or __int128 needed. There is no integer overflow; the runtime automatically promotes to a big-integer representation when needed. Underscores are allowed in numeric literals for readability: 1_000_000.
Integer methods
#include <stdio.h> int main(void) { int number = 42; /* No methods on primitives — use operators or <math.h> */ printf("%s\n", number % 2 == 0 ? "even" : "odd"); printf("%s\n", number > 0 ? "positive" : "not positive"); for (int count = 0; count < 3; count++) { printf("tick\n"); } return 0; }
number = 42 puts number.even? # true puts number.odd? # false puts number.positive? # true puts number.zero? # false puts number.abs # 42 puts number.digits # [2, 4] — reversed decimal digits 3.times { puts "tick" } 1.upto(5) { |count| puts count }
In Ruby, integers are objects with a rich method API. Predicate methods ending in ? return true or false: even?, odd?, positive?, negative?, zero?. The times method replaces for (int i = 0; i < n; i++) for simple counting loops, and upto / downto handle ranges.
Float — one type for all real numbers
#include <stdio.h> #include <math.h> int main(void) { float single_prec = 3.14f; double double_prec = 3.14159265358979; printf("%.2f\n", (double)single_prec); printf("%.15f\n", double_prec); printf("%.4f\n", sqrt(2.0)); return 0; }
# Ruby has one Float type — IEEE 754 double precision price = 3.14 puts price.class # Float puts price.round(2) # 3.14 puts Math.sqrt(2).round(4) puts 1.0 / 3 # 0.3333... puts price.infinite? # nil (it is finite) puts (1.0 / 0).infinite? # 1 (positive infinity)
Ruby has a single Float class (IEEE 754 double precision — same as C's double). There is no float vs double distinction. Math functions live in the Math module: Math.sqrt, Math.sin, Math::PI. Float::INFINITY and division by zero produce infinity, not undefined behavior.
Integer division and modulo
#include <stdio.h> int main(void) { printf("%d\n", 10 / 3); /* integer division: 3 */ printf("%d\n", 10 % 3); /* remainder: 1 */ printf("%.4f\n", 10.0 / 3.0); /* float division */ printf("%d\n", -7 % 3); /* implementation-defined in C89, -1 in C99+ */ return 0; }
puts 10 / 3 # integer division: 3 puts 10 % 3 # remainder: 1 puts 10.0 / 3 # float division: 3.333... puts -7 % 3 # always non-negative in Ruby: 2 puts 2 ** 10 # exponentiation: 1024
Ruby's / on two integers performs floor division — it rounds toward negative infinity: -7 / 2 == -4 in Ruby. C99 truncates toward zero: -7 / 2 == -3 in C. These differ for negative operands and is a common source of bugs when porting C logic to Ruby. The % modulo in Ruby always returns a non-negative result when the divisor is positive; in C99 the sign follows the truncation, so -7 % 3 == -1. The ** operator handles exponentiation; there is no pow() call needed.
Numeric conversions
#include <stdio.h> #include <stdlib.h> int main(void) { /* string to int: atoi, strtol */ int parsed = atoi("42"); /* int to string: snprintf */ char buffer[32]; snprintf(buffer, sizeof(buffer), "%d", parsed); printf("%d %s\n", parsed, buffer); printf("%.1f\n", (double)parsed); /* int to float */ return 0; }
parsed = "42".to_i puts parsed puts parsed.to_s puts parsed.to_f puts parsed.to_r # rational: 42/1 puts "3.14".to_f puts "0xFF".to_i(16) # hex parsing: 255 puts 255.to_s(16) # to hex string: "ff"
Ruby's conversion methods follow a consistent naming convention: to_i (integer), to_f (float), to_s (string), to_r (rational). The to_i method accepts a radix argument: "0xFF".to_i(16) parses hexadecimal. These replace atoi, strtol, strtod, and snprintf — all without including any headers.
Arrays
Creating arrays — dynamic, not fixed-size
#include <stdio.h> int main(void) { /* Fixed size, element type, manual length tracking */ int numbers[5] = {1, 2, 3, 4, 5}; char *fruits[] = {"apple", "banana", "cherry"}; size_t fruit_count = 3; printf("%d\n", numbers[0]); printf("%zu\n", fruit_count); return 0; }
numbers = [1, 2, 3, 4, 5] fruits = ["apple", "banana", "cherry"] mixed = [1, "two", :three, nil, 3.14] puts numbers[0] puts fruits.length puts mixed.inspect puts numbers.class
Ruby arrays are heap-allocated, dynamically sized, and heterogeneous — one array can hold integers, strings, symbols, and nil simultaneously. There is no fixed capacity, no element type restriction, and no manual length tracking. The %w[word list here] shorthand creates an array of strings without quotation marks.
Accessing elements and slicing
#include <stdio.h> int main(void) { int numbers[] = {10, 20, 30, 40, 50}; size_t length = 5; printf("%d\n", numbers[0]); /* first */ printf("%d\n", numbers[length - 1]); /* last */ /* slice: manual copy into new array */ for (size_t index = 1; index <= 3; index++) { printf("%d ", numbers[index]); } printf("\n"); return 0; }
numbers = [10, 20, 30, 40, 50] puts numbers[0] # first: 10 puts numbers[-1] # last: 50 (negative index from end) puts numbers[-2] # second-to-last: 40 puts numbers[1, 3].inspect # start, length: [20, 30, 40] puts numbers[1..3].inspect # inclusive range: [20, 30, 40] puts numbers.first puts numbers.last
Ruby arrays support negative indexing: array[-1] is the last element, array[-2] the second-to-last. This eliminates the common C pattern of array[length - 1]. Slicing returns a new array — no manual allocation or copy. An out-of-bounds access returns nil rather than undefined behavior.
push, pop, shift, unshift
#include <stdio.h> #include <stdlib.h> #include <string.h> /* Growable array requires manual realloc */ int main(void) { int *items = malloc(2 * sizeof(int)); items[0] = 1; items[1] = 2; /* append: realloc + assign */ items = realloc(items, 3 * sizeof(int)); items[2] = 3; printf("%d\n", items[2]); free(items); return 0; }
items = [2, 3] items.push(4) # add to end — also: items << 4 items.unshift(1) # add to front puts items.inspect # [1, 2, 3, 4] last = items.pop # remove from end first = items.shift # remove from front puts last puts first puts items.inspect # [2, 3]
Ruby arrays grow and shrink automatically — no realloc, no capacity management, no free. push (or <<) appends to the end; pop removes from the end. unshift prepends to the front; shift removes from the front. These map to the standard stack and queue operations without any memory management ceremony.
map, select, reject
#include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5, 6}; /* map: allocate new array, loop, transform */ int doubled[6]; for (int index = 0; index < 6; index++) { doubled[index] = numbers[index] * 2; } for (int index = 0; index < 6; index++) { printf("%d ", doubled[index]); } printf("\n"); return 0; }
numbers = [1, 2, 3, 4, 5, 6] doubled = numbers.map { |number| number * 2 } evens = numbers.select { |number| number.even? } odds = numbers.reject { |number| number.even? } puts doubled.inspect puts evens.inspect puts odds.inspect
Ruby's Enumerable module provides functional collection operations built into every array. map transforms each element and returns a new array; select keeps elements matching the block; reject keeps elements that do not match. No temporary arrays to allocate, no index management, no explicit loops.
reduce / inject — fold to a single value
#include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5}; int total = 0; for (int index = 0; index < 5; index++) { total += numbers[index]; } int product = 1; for (int index = 0; index < 5; index++) { product *= numbers[index]; } printf("%d %d\n", total, product); return 0; }
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |sum, number| sum + number } product = numbers.inject(:*) # symbol shorthand for the * method puts total # 15 puts product # 120 puts numbers.sum # built-in shorthand
reduce (alias: inject) folds an array into a single value by applying a block to an accumulator and each element in turn. The first argument is the initial accumulator value. The symbol shorthand inject(:+) is equivalent to inject { |accumulator, value| accumulator + value }. sum is a built-in convenience method for the common addition case.
sort, uniq, flatten, compact
#include <stdio.h> #include <stdlib.h> int compare_ints(const void *left, const void *right) { return (*(int *)left - *(int *)right); } int main(void) { int numbers[] = {3, 1, 4, 1, 5, 9}; qsort(numbers, 6, sizeof(int), compare_ints); for (int index = 0; index < 6; index++) { printf("%d ", numbers[index]); } printf("\n"); return 0; }
numbers = [3, 1, 4, 1, 5, 9] puts numbers.sort.inspect puts numbers.sort.reverse.inspect puts numbers.uniq.inspect # remove duplicates nested = [[1, 2], [3, [4, 5]]] puts nested.flatten.inspect # recursively unwrap with_nils = [1, nil, 2, nil, 3] puts with_nils.compact.inspect # remove nil values
sort returns a sorted copy (uses the <=> spaceship operator for comparison — no comparator function pointer needed). uniq removes duplicates, preserving order. flatten recursively expands nested arrays — there is no C equivalent at all. compact removes nil values. Methods ending in ! mutate in place: sort!, uniq!.
Hashes
Creating hashes — no C stdlib equivalent
#include <stdio.h> /* C has no built-in hash map */ /* Common approaches: parallel arrays, third-party libraries */ typedef struct { const char *key; int value; } Entry; int main(void) { Entry scores[] = {{"alice", 95}, {"bob", 87}}; size_t count = 2; for (size_t index = 0; index < count; index++) { printf("%s: %d\n", scores[index].key, scores[index].value); } return 0; }
# Hash literals with symbol keys (most common) scores = { alice: 95, bob: 87, carol: 91 } puts scores[:alice] puts scores.size # String keys config = { "host" => "localhost", "port" => 5432 } puts config["host"]
Ruby has a built-in Hash class — a key-value store with O(1) average lookup. The { key: value } syntax uses symbol keys (the most common style). The { "key" => value } syntax accepts any object as a key. Hashes maintain insertion order (since Ruby 1.9). There is no need for third-party libraries or manual parallel arrays.
Accessing, setting, and fetching
#include <stdio.h> #include <string.h> typedef struct { const char *key; int value; } Entry; static int lookup(Entry *entries, size_t count, const char *target) { for (size_t index = 0; index < count; index++) { if (strcmp(entries[index].key, target) == 0) return entries[index].value; } return -1; /* sentinel for "not found" */ } int main(void) { Entry config[] = {{"timeout", 30}, {"retries", 3}}; printf("%d\n", lookup(config, 2, "timeout")); printf("%d\n", lookup(config, 2, "missing")); /* -1 */ return 0; }
config = { timeout: 30, retries: 3 } config[:timeout] = 60 # set / update puts config[:timeout] puts config[:missing].inspect # nil — not found, no crash # fetch raises KeyError on missing key begin config.fetch(:missing) rescue KeyError => error puts error.message end puts config.fetch(:missing, "default")
Accessing a missing key returns nil rather than a sentinel value or undefined behavior. fetch is the strict version: fetch(:key) raises KeyError when the key is absent; fetch(:key, default) returns the default. Use bracket access when absence is expected; use fetch when absence means a programming error.
Iterating over a hash
#include <stdio.h> typedef struct { const char *name; int score; } Player; int main(void) { Player players[] = {{"alice", 95}, {"bob", 87}, {"carol", 91}}; size_t count = 3; for (size_t index = 0; index < count; index++) { printf("%s: %d\n", players[index].name, players[index].score); } return 0; }
scores = { alice: 95, bob: 87, carol: 91 } scores.each { |name, score| puts "#{name}: #{score}" } puts scores.keys.inspect puts scores.values.inspect puts scores.any? { |_name, score| score > 90 } puts scores.select { |_name, score| score >= 90 }.inspect
each on a hash yields two-element key-value pairs that Ruby automatically destructures into block parameters. Hashes include the full Enumerable module: map, select, reject, any?, all?, min_by, sort_by. The keys and values methods return plain arrays.
Default values and merge
#include <stdio.h> #include <string.h> int main(void) { /* Word count: manual initialization required */ const char *words[] = {"apple", "banana", "apple", "cherry", "apple"}; /* (Tracking counts for arbitrary keys requires a hash table) */ printf("no built-in hash map\n"); return 0; }
word_count = Hash.new(0) # default value 0 for missing keys words = ["apple", "banana", "apple", "cherry", "apple"] words.each { |word| word_count[word] += 1 } puts word_count.inspect defaults = { timeout: 30, retries: 3, debug: false } overrides = { timeout: 60 } merged = defaults.merge(overrides) puts merged.inspect
Hash.new(default) returns the default value for any missing key, making accumulation patterns like word counting a one-liner. For a computed default (e.g. a new array per key), use a block: Hash.new { |hash, key| hash[key] = [] }. merge combines two hashes, with the rightmost values winning on conflicts.
Ranges
Range creation
#include <stdio.h> int main(void) { /* C: no range type — use for loop or manual check */ for (int n = 1; n <= 10; n++) { /* process n */ } int value = 7; printf("%s\n", value >= 1 && value <= 10 ? "in range" : "out of range"); return 0; }
inclusive = (1..10) # includes 10 exclusive = (1...10) # excludes 10 puts inclusive.to_a.inspect puts exclusive.to_a.inspect puts inclusive.include?(7) puts inclusive.min puts inclusive.max puts inclusive.sum
Ruby ranges are first-class objects: 1..10 (inclusive) and 1...10 (exclusive). A range can be iterated, queried for membership, converted to an array, and used in pattern matching. There is no C equivalent — ranges must be simulated with arithmetic or loops. Character ranges also work: ("a".."z").
Iterating over ranges
#include <stdio.h> int main(void) { /* Counting loop */ for (int number = 1; number <= 5; number++) { printf("%d\n", number * number); } /* Character range */ for (char letter = 'a'; letter <= 'e'; letter++) { printf("%c ", letter); } printf("\n"); return 0; }
(1..5).each { |number| puts number * number } ("a".."e").each { |letter| print "#{letter} " } puts # Ranges support the full Enumerable API puts (1..10).select(&:even?).inspect puts (1..10).map { |number| number ** 2 }.first(3).inspect
Ranges support each and the entire Enumerable suite: map, select, reduce, first, sum, etc. String ranges iterate character-by-character using Ruby's lexicographic successor. The step method adds stride control: (1..20).step(5) visits 1, 6, 11, 16.
Ranges in case / when
#include <stdio.h> int main(void) { int score = 78; const char *grade; if (score >= 90) grade = "A"; else if (score >= 80) grade = "B"; else if (score >= 70) grade = "C"; else if (score >= 60) grade = "D"; else grade = "F"; printf("%s\n", grade); return 0; }
score = 78 grade = case score when 90..100 then "A" when 80..89 then "B" when 70..79 then "C" when 60..69 then "D" else "F" end puts grade
Ranges work as when clauses in case expressions because Ruby calls === (case equality) on each pattern. case in Ruby returns the matched value, so the entire construct is an expression that can be assigned directly. This is far cleaner than the cascading if/else if required in C.
Control Flow
if / elsif / else
#include <stdio.h> int main(void) { int temperature = 22; if (temperature > 30) { printf("hot\n"); } else if (temperature > 20) { printf("warm\n"); } else if (temperature > 10) { printf("cool\n"); } else { printf("cold\n"); } return 0; }
temperature = 22 if temperature > 30 puts "hot" elsif temperature > 20 puts "warm" elsif temperature > 10 puts "cool" else puts "cold" end
Ruby's keyword is elsif (not else if). No parentheses around the condition, no curly braces around the body — just end. if is an expression and returns the matched branch's value: label = if flag then "yes" else "no" end. The parentheses in C's if (condition) are mandatory syntax; in Ruby they are optional stylistic noise.
unless — negated if
#include <stdio.h> #include <stdbool.h> int main(void) { bool authenticated = false; if (!authenticated) { printf("Please log in\n"); } return 0; }
authenticated = false unless authenticated puts "Please log in" end # Same as: puts "Please log in" if !authenticated # Postfix unless (even more idiomatic): puts "Please log in" unless authenticated
unless condition is equivalent to if !condition — it reads more naturally for negative guards. Postfix unless places the condition after the action: do_something unless error. Avoid using unless with an else clause or with a negative condition — it becomes difficult to reason about.
Ternary and postfix if
#include <stdio.h> int main(void) { int count = 5; const char *label = count == 1 ? "item" : "items"; printf("%d %s\n", count, label); int debug = 1; if (debug) printf("debug mode on\n"); return 0; }
count = 5 label = count == 1 ? "item" : "items" puts "#{count} #{label}" debug = true puts "debug mode on" if debug # if / unless both have postfix form: raise "too many!" if count > 100
Ruby has the same ternary operator as C: condition ? value_if_true : value_if_false. Postfix if and unless are idiomatic for single-line guards: statement if condition. The postfix form is preferred over a full if...end block when there is only one line to execute.
while / until
#include <stdio.h> int main(void) { int counter = 0; while (counter < 5) { printf("%d\n", counter); counter++; } /* C: do-while for guaranteed first execution */ do { printf("once\n"); } while (0); return 0; }
counter = 0 while counter < 5 puts counter counter += 1 end # until runs while the condition is FALSE counter = 5 until counter == 0 counter -= 1 end puts counter # Guaranteed first execution: loop do puts "once" break end
while loops run while the condition is true; until runs while it is false (equivalent to while !condition). Ruby has no do...while construct — use loop do ... break if condition end for a guaranteed first execution. Note that ++ and -- do not exist in Ruby; use += 1 and -= 1.
case / when — no fallthrough
#include <stdio.h> int main(void) { int direction = 1; switch (direction) { case 1: printf("north\n"); break; case 2: printf("south\n"); break; case 3: case 4: printf("east or west\n"); break; default: printf("unknown\n"); } return 0; }
direction = "north" case direction when "north" puts "Going up" when "south" puts "Going down" when "east", "west" # comma-separated alternatives puts "Going sideways" when /^n/ # regex match puts "Starts with N" else puts "Unknown" end
Ruby's case/when has no fallthrough — there is no break required. Multiple patterns in one when are comma-separated. Each when uses === (case equality) for matching, enabling regex patterns, range membership, and class type checks in addition to literal equality.
loop, break, next
#include <stdio.h> int main(void) { for (int position = 1; ; position++) { if (position % 2 == 0) continue; if (position > 10) break; printf("%d\n", position); } return 0; }
position = 0 loop do position += 1 next if position.even? # skip even — like C's continue break if position > 10 # exit loop — like C's break puts position end
loop do ... end is Ruby's infinite loop — equivalent to for (;;) or while (1). break exits the loop; next skips to the next iteration (C's continue). Both break and next work inside blocks too: next inside each skips to the next element, and break stops the entire iteration.
Methods
def — implicit return
#include <stdio.h> static int square(int number) { return number * number; } static const char *greet(const char *name) { static char buffer[64]; snprintf(buffer, sizeof(buffer), "Hello, %s!", name); return buffer; } int main(void) { printf("%d\n", square(7)); printf("%s\n", greet("Alice")); return 0; }
def square(number) number * number # last expression is the return value end def greet(name) "Hello, #{name}!" # no return keyword needed end puts square(7) puts greet("Alice")
Ruby methods return the value of their last expression — no return keyword is required (though it can be used for early exit). There is no return type declaration, no static, and no static buffer trick for returning strings. Methods at the top level become private methods of Object. The method body is indented 2 spaces; end closes it.
Default parameter values
#include <stdio.h> /* C has no default parameters — use separate overloads or sentinel values */ static void greet_with(const char *name, const char *greeting) { printf("%s, %s!\n", greeting, name); } static void greet(const char *name) { greet_with(name, "Hello"); } int main(void) { greet("Alice"); greet_with("Bob", "Hi"); return 0; }
def greet(name, greeting = "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", "Hi")
Ruby supports default parameter values directly in the method signature — no wrapper function, no sentinel value, no overloads. Default values are evaluated at call time, not definition time, so dynamic defaults like def log(message, time = Time.now) work as expected. Multiple parameters can have defaults, as long as they come after required parameters.
Keyword arguments
#include <stdio.h> /* C: no keyword args — must remember positional order */ static void create_user(const char *name, int age, const char *city) { printf("%s, age %d, from %s\n", name, age, city); } int main(void) { create_user("Alice", 30, "Paris"); /* which is which? */ return 0; }
def create_user(name:, age:, city: "Unknown") puts "#{name}, age #{age}, from #{city}" end create_user(name: "Alice", age: 30, city: "Paris") create_user(age: 25, name: "Bob") # order doesn't matter
Ruby keyword arguments are declared with a colon after the parameter name. At the call site, the argument names are visible and order-independent. Required keyword arguments (no default) raise ArgumentError if omitted. This self-documenting call style eliminates the positional ambiguity of C function calls with multiple parameters of the same type.
*args — variadic arguments
#include <stdio.h> #include <stdarg.h> static void print_all(int count, ...) { va_list arguments; va_start(arguments, count); for (int index = 0; index < count; index++) { printf("%d\n", va_arg(arguments, int)); } va_end(arguments); } int main(void) { print_all(3, 10, 20, 30); return 0; }
def print_all(*items) items.each { |item| puts item } end def describe(**attributes) attributes.each { |key, value| puts "#{key}: #{value}" } end print_all(10, 20, 30) describe(name: "Alice", age: 30)
*args in a Ruby method signature collects any number of positional arguments into an array — type-safe and without the manual va_list machinery. **kwargs collects keyword arguments into a hash. The splat works at the call site too: print_all(*items_array) spreads an array into positional arguments.
One-liner method syntax (Ruby 3.0+)
#include <stdio.h> static int double_it(int number) { return number * 2; } static int triple_it(int number) { return number * 3; } int main(void) { printf("%d %d\n", double_it(5), triple_it(5)); return 0; }
def double_it(number) = number * 2 def triple_it(number) = number * 3 puts double_it(5) puts triple_it(5)
Ruby 3.0 introduced an endless method syntax: def name(params) = expression. It reads like a mathematical definition and is equivalent to a normal method with an implicit return. This is closer to C's single-expression function bodies ({ return expr; }) but without the braces or return. Use it for simple, single-expression methods.
Blocks & Iterators
Blocks — anonymous code passed to a method
#include <stdio.h> typedef void (*callback)(const char *name); static void greet_each(const char **names, size_t count, callback action) { for (size_t index = 0; index < count; index++) { action(names[index]); } } static void say_hello(const char *name) { printf("Hello, %s!\n", name); } int main(void) { const char *names[] = {"Alice", "Bob", "Carol"}; greet_each(names, 3, say_hello); return 0; }
names = ["Alice", "Bob", "Carol"] # do...end block for multi-line names.each do |name| puts "Hello, #{name}!" end # { } block for single-line names.each { |name| puts "Hello, #{name}!" }
A Ruby block is an anonymous chunk of code passed to a method — similar in spirit to a C function pointer, but more ergonomic. The do |params| ... end form is conventional for multi-line blocks; { |params| ... } for single-line. Blocks are passed implicitly — they are not a declared parameter in the method signature. yield inside a method invokes the block.
yield — invoking the caller's block
#include <stdio.h> typedef void (*action)(int count); static void repeat(int times, action callback) { for (int count = 0; count < times; count++) { callback(count); } } static void print_step(int count) { printf("Step %d\n", count); } int main(void) { repeat(3, print_step); return 0; }
def repeat(times) times.times { |count| yield count } end repeat(3) { |count| puts "Step #{count}" } # block_given? checks whether a block was provided def maybe_transform(value) block_given? ? yield(value) : value end puts maybe_transform(5) { |number| number * 2 } puts maybe_transform(5)
yield transfers control to the caller's block, optionally passing arguments. Any method can accept a block — no special parameter is required in the signature. block_given? checks whether the caller provided one. This is the mechanism behind all of Ruby's iterators: each, map, and select all call yield internally.
Proc and lambda — blocks as objects
#include <stdio.h> typedef int (*transformer)(int); static int apply(int value, transformer callback) { return callback(value); } static int double_it(int number) { return number * 2; } int main(void) { printf("%d\n", apply(5, double_it)); /* function pointers lack closures */ return 0; }
doubler = proc { |number| number * 2 } tripler = lambda { |number| number * 3 } strict = ->(number) { number * 4 } # lambda shorthand puts doubler.call(5) puts tripler.call(5) puts strict.(5) # alternative call syntax # Symbol-to-proc shorthand puts [1, 2, 3].map(&:to_s).inspect
A Proc object stores a block as a value that can be called later, passed around, and returned from methods. The ->(params) { body } syntax creates a lambda — a stricter form that enforces argument count and treats return as returning from the lambda itself (not the enclosing method). The &:method_name shorthand converts a symbol to a block that calls that method on each element.
Enumerable predicates — all?, any?, find, count
#include <stdio.h> #include <stdbool.h> int main(void) { int numbers[] = {2, 4, 6, 7, 8}; size_t count = 5; bool all_even = true; for (size_t index = 0; index < count; index++) { if (numbers[index] % 2 != 0) { all_even = false; break; } } printf("%s\n", all_even ? "all even" : "not all even"); return 0; }
numbers = [2, 4, 6, 7, 8] puts numbers.all? { |number| number.even? } puts numbers.any? { |number| number > 5 } puts numbers.none? { |number| number > 10 } puts numbers.count { |number| number > 4 } puts numbers.find { |number| number > 5 } puts numbers.min puts numbers.max
The Enumerable module provides predicate methods that eliminate manual loops for common queries: all?, any?, none?, one?. find returns the first matching element (or nil). count with a block counts matching elements. min, max, and sum work directly without a loop. All these are available on any collection that implements each.
Chaining enumerable operations
#include <stdio.h> int main(void) { int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int total = 0; for (int index = 0; index < 10; index++) { if (numbers[index] % 2 == 0) { total += numbers[index] * numbers[index]; } } printf("%d\n", total); return 0; }
numbers = (1..10).to_a result = numbers .select { |number| number.even? } .map { |number| number ** 2 } .sum puts result
Each chained method returns a new array that the next method operates on. The chain reads left-to-right, closely matching the intent: "take the numbers, keep the even ones, square them, sum the result." The entire pipeline allocates intermediate arrays; use Enumerator::Lazy for large datasets where intermediate collections would be wasteful.
Classes & OOP
Class vs struct
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char name[64]; double balance; } BankAccount; static void bank_account_init(BankAccount *account, const char *name, double balance) { strncpy(account->name, name, 63); account->balance = balance; } static void bank_account_print(const BankAccount *account) { printf("%s: $%.2f\n", account->name, account->balance); } int main(void) { BankAccount account; bank_account_init(&account, "Alice", 1000.0); bank_account_print(&account); return 0; }
class BankAccount def initialize(name, balance) @name = name # instance variable @balance = balance end def to_s "#{@name}: $#{@balance}" end end account = BankAccount.new("Alice", 1000.0) puts account
Ruby classes bundle data and methods together — no separate struct definition, no manual method dispatch table. Instance variables start with @ and are automatically created on first assignment in initialize. BankAccount.new allocates the object and calls initialize. to_s is Ruby's toString equivalent; puts object calls it automatically.
attr_accessor — generated getters and setters
#include <stdio.h> #include <string.h> typedef struct { char name[64]; int age; } Person; static const char *person_name(const Person *person) { return person->name; } static int person_age(const Person *person) { return person->age; } static void person_set_name(Person *person, const char *name) { strncpy(person->name, name, 63); } int main(void) { Person person = {"Alice", 30}; person_set_name(&person, "Alicia"); printf("%s, %d\n", person_name(&person), person_age(&person)); return 0; }
class Person attr_accessor :name # getter + setter attr_reader :age # getter only (read-only) def initialize(name, age) @name = name @age = age end end person = Person.new("Alice", 30) person.name = "Alicia" # calls name= setter puts "#{person.name}, #{person.age}"
attr_accessor generates both a getter and a setter for an instance variable. attr_reader generates only a getter. attr_writer only a setter. Setters follow the name= naming convention — person.name = "Alicia" calls the name= method. This replaces every manual getter/setter function from the C struct pattern.
Inheritance — less ceremony than vtables
#include <stdio.h> #include <string.h> /* C OOP via function pointers — verbose */ typedef struct Animal Animal; struct Animal { char name[32]; const char *(*speak)(const Animal *); }; static const char *animal_speak(const Animal *animal) { return "..."; } int main(void) { Animal animal = {"Rex", animal_speak}; printf("%s says: %s\n", animal.name, animal.speak(&animal)); return 0; }
class Animal def initialize(name) @name = name end def speak "#{@name} makes a sound" end end class Dog < Animal def speak "#{@name} barks" end end animal = Dog.new("Rex") puts animal.speak puts animal.is_a?(Animal)
Ruby uses < for inheritance: class Dog < Animal. Overriding a method is simply redefining it — no vtable, no function pointer, no virtual keyword. super calls the parent's version. Ruby supports single inheritance only, but modules serve as mixins for sharing behavior across unrelated classes.
Class methods — self.method_name
#include <stdio.h> typedef struct { int id; } Widget; static int widget_count = 0; static Widget widget_create(void) { Widget widget; widget.id = ++widget_count; return widget; } int main(void) { Widget first = widget_create(); Widget second = widget_create(); printf("count=%d id1=%d\n", widget_count, first.id); return 0; }
class Widget @@count = 0 # class variable shared across all instances def initialize @@count += 1 @id = @@count end def self.count # class method — like a C static function @@count end def id @id end end first = Widget.new second = Widget.new puts Widget.count puts first.id
Class methods are defined with self.method_name — equivalent to C static functions scoped to the struct. Class variables @@name are shared across all instances and subclasses. In practice, Ruby developers often prefer class-level instance variables (@count on the class itself) to avoid inheritance edge cases with @@.
Open classes — add methods to existing types
#include <stdio.h> #include <string.h> /* C: no way to add methods to primitive types */ static int string_is_palindrome(const char *text) { size_t length = strlen(text); for (size_t index = 0; index < length / 2; index++) { if (text[index] != text[length - 1 - index]) return 0; } return 1; } int main(void) { printf("%d\n", string_is_palindrome("racecar")); return 0; }
# Add a method to Ruby's built-in String class class String def palindrome? self == self.reverse end end puts "racecar".palindrome? # true puts "hello".palindrome? # false # Add methods to Integer too class Integer def factorial return 1 if self <= 1 self * (self - 1).factorial end end puts 5.factorial
Ruby classes are always "open" — any class, including built-in types like String, Integer, and Array, can be reopened and extended at any time. This is called monkey patching. C has no equivalent: built-in types are not extensible. Used with care, open classes make code read naturally; used carelessly, they can cause surprising conflicts.
Modules & Mixins
Module as namespace
#include <stdio.h> #include <math.h> /* C uses prefixes to avoid name collisions */ static double geometry_circle_area(double radius) { return M_PI * radius * radius; } int main(void) { printf("%.4f\n", geometry_circle_area(5.0)); return 0; }
module Geometry PI = Math::PI class Circle def self.area(radius) PI * radius ** 2 end end class Rectangle def self.area(width, height) width * height end end end puts Geometry::Circle.area(5.0).round(4) puts Geometry::Rectangle.area(3, 4)
Ruby modules serve as namespaces — grouping related constants and classes under a name, similar to C's naming prefix convention. The :: operator accesses constants inside a module. Unlike C's convention-based prefixing, Ruby namespaces are enforced by the runtime. Modules can be nested arbitrarily deep.
include — module as mixin
#include <stdio.h> /* C: "shared behavior" is a free function — every caller must pass it explicitly */ typedef struct { const char *name; } Named; static void greet(const Named *object) { printf("Hello, I am %s\n", object->name); } int main(void) { Named person = {"Alice"}; Named robot = {"R2D2"}; greet(&person); greet(&robot); /* No module system — every call site wires up the function manually */ return 0; }
module Greetable def greet "Hello, I am #{name}" end end class Person include Greetable attr_reader :name def initialize(name) @name = name end end class Robot include Greetable attr_reader :name def initialize(name) @name = name end end puts Person.new("Alice").greet puts Robot.new("R2D2").greet
include ModuleName mixes all of a module's methods into a class — a form of multiple inheritance without the diamond-problem ambiguity. Modules are Ruby's answer to interfaces (with default implementations). A class can include any number of modules. This is far cleaner than C's #include hacks or function pointer tables for sharing behavior.
Comparable — implementing <=> for free comparisons
#include <stdio.h> #include <stdlib.h> typedef struct { char name[32]; int score; } Player; static int compare_players(const void *left, const void *right) { return ((Player *)left)->score - ((Player *)right)->score; } int main(void) { Player players[] = {{"Alice", 95}, {"Bob", 70}, {"Carol", 85}}; qsort(players, 3, sizeof(Player), compare_players); for (int i = 0; i < 3; i++) { printf("%s: %d\n", players[i].name, players[i].score); } return 0; }
class Player include Comparable attr_reader :name, :score def initialize(name, score) @name = name @score = score end def <=>(other) # define this one method score <=> other.score end end players = [Player.new("Alice", 95), Player.new("Bob", 70), Player.new("Carol", 85)] puts players.sort.map(&:name).inspect puts players.min.name puts players.max.name
Including Comparable and implementing the single <=> (spaceship) operator automatically provides <, >, <=, >=, ==, between?, and clamp — replacing the manual comparator function from C's qsort. The spaceship operator returns -1, 0, or 1 for less-than, equal, or greater-than.
Error Handling
Exceptions vs error codes
#include <stdio.h> #include <errno.h> #include <string.h> static int divide(int dividend, int divisor, int *result) { if (divisor == 0) { errno = EDOM; return -1; } *result = dividend / divisor; return 0; } int main(void) { int result; if (divide(10, 0, &result) != 0) { printf("error: %s\n", strerror(errno)); } else { printf("%d\n", result); } return 0; }
def divide(dividend, divisor) raise ArgumentError, "Division by zero" if divisor == 0 dividend / divisor end begin puts divide(10, 2) puts divide(10, 0) # raises rescue ArgumentError => error puts "Error: #{error.message}" end
Ruby uses exceptions instead of error codes. raise throws an exception object; rescue catches it. The call site is clean — no out-parameters, no return value check, no errno. Exceptions propagate up the call stack until caught; if uncaught, they terminate the program with a backtrace. The begin...rescue...end block is the try/catch equivalent.
begin / rescue / ensure
#include <stdio.h> #include <stdlib.h> int main(void) { /* C: cleanup with goto or nested ifs */ void *resource = malloc(1024); if (!resource) { printf("allocation failed\n"); return 1; } printf("using resource\n"); free(resource); /* always free, even on error paths */ return 0; }
begin resource = "opened" puts "using #{resource}" raise "something went wrong" rescue RuntimeError => error puts "Caught: #{error.message}" rescue => error puts "Other error: #{error.message}" ensure puts "cleanup always runs (like free())" end
rescue catches specific exception types; the bare rescue => catches any StandardError. ensure always executes after the begin block — whether it succeeded, raised, or was rescued — equivalent to C's cleanup code that must run regardless of the error path. Ruby's GC frees memory automatically, so ensure is mainly for closing files and releasing external resources.
Custom exception classes
#include <stdio.h> /* C: error codes in an enum */ typedef enum { ERR_NONE = 0, ERR_NOT_FOUND, ERR_INVALID_INPUT, ERR_NETWORK, } ErrorCode; int main(void) { ErrorCode code = ERR_NOT_FOUND; if (code == ERR_NOT_FOUND) { printf("not found (code %d)\n", code); } return 0; }
class NetworkError < StandardError; end class NotFoundError < NetworkError def initialize(resource) super("#{resource} not found") @resource = resource end attr_reader :resource end begin raise NotFoundError.new("user#42") rescue NotFoundError => error puts error.message puts error.resource rescue NetworkError => error puts "Network problem" end
Custom exceptions are just classes that inherit from StandardError (or another exception class). The inheritance hierarchy lets you rescue at different levels of specificity — catching NetworkError will also catch NotFoundError. Exception classes can carry additional data (like @resource), replacing the need for separate error-detail parameters.
raise, retry, and return from ensure
#include <stdio.h> /* C: retry loops require manual flags or goto */ static int connect(int *attempts) { (*attempts)++; if (*attempts < 3) return -1; /* simulate failure */ return 0; /* success */ } int main(void) { int attempts = 0; while (connect(&attempts) != 0 && attempts < 3) { printf("retrying (%d)...\n", attempts); } printf("connected after %d attempt(s)\n", attempts); return 0; }
attempts = 0 begin attempts += 1 raise "connection failed" if attempts < 3 puts "connected after #{attempts} attempt(s)" rescue RuntimeError => error puts "retrying (#{attempts})..." retry if attempts < 3 puts "giving up: #{error.message}" end
retry inside a rescue block re-executes the entire begin block from the top — eliminating the manual loop-and-flag pattern required in C. raise with no arguments re-raises the current exception. raise with a string creates a RuntimeError. raise with an exception class and message creates a specific exception.
Memory Management
Garbage collection vs malloc / free
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { /* Programmer is responsible for every allocation */ char *message = malloc(64); if (!message) return 1; strncpy(message, "Hello from the heap", 63); printf("%s\n", message); free(message); /* must free — or memory leaks */ /* Use-after-free, double-free → undefined behavior */ return 0; }
# Ruby: allocate freely, GC handles cleanup message = "Hello from the heap" puts message # Objects are GC'd when unreachable — no free() needed 1000.times do |number| temporary = "object #{number}" # allocated each iteration end # eligible for GC after loop puts "no memory leaks, no manual free()"
Ruby uses a garbage collector — the runtime automatically reclaims memory for objects that are no longer reachable. There is no malloc, no free, no use-after-free, no double-free, and no memory leak from forgetting to clean up. The trade-off is that GC adds runtime overhead and introduces occasional pause times — acceptable for most applications, not for hard real-time systems.
Symbols — interned, identity-based values
#include <stdio.h> #include <string.h> int main(void) { /* C: compare strings by content */ const char *status = "active"; if (strcmp(status, "active") == 0) { printf("is active\n"); } /* Two "active" literals may or may not share memory */ return 0; }
status = :active puts status.class puts status == :active # identity check — same object puts :active.object_id == :active.object_id # always true # Symbols vs strings puts :active.to_s # "active" puts "active".to_sym # :active # Common uses: hash keys, method names, options config = { mode: :production, retries: 3 } puts config[:mode]
Ruby symbols (:name) are immutable, interned identifiers. Every occurrence of the same symbol literal in a program is the same object — identity comparison is O(1). Unlike strings, symbols never need allocation or duplication for comparison. They are used for hash keys, method names, and option values — anywhere a stable, lightweight label is needed.
freeze — opt-in immutability
#include <stdio.h> int main(void) { /* C: const applies to the pointer, not always the data */ const int value = 42; /* value = 43; // compile error */ const char *str = "hello"; /* str[0] = 'H'; // undefined behavior */ char mutable_str[] = "hello"; /* copy — mutable */ mutable_str[0] = 'H'; printf("%s\n", mutable_str); return 0; }
value = 42.freeze puts value.frozen? # true config = { host: "localhost", port: 5432 }.freeze puts config[:host] begin config[:port] = 3306 # attempt mutation rescue FrozenError => error puts error.message end CONSTANT = "immutable".freeze # frozen constant string
Calling .freeze on any Ruby object makes it permanently immutable — analogous to const in C, but applied at runtime to any object. Attempting to mutate a frozen object raises FrozenError. In Ruby 4.0, string literals are frozen by default. Freezing is useful for constants, configuration hashes, and objects passed across thread boundaries.
Pattern Matching
case / in — structural pattern matching
#include <stdio.h> /* C: manual type dispatch with if/else or switch */ typedef enum { TYPE_INT, TYPE_STRING, TYPE_ARRAY } ValueType; typedef struct { ValueType type; union { int integer; const char *string; } data; } Value; int main(void) { Value value = { .type = TYPE_INT, .data = { .integer = 42 } }; switch (value.type) { case TYPE_INT: printf("Integer: %d\n", value.data.integer); break; case TYPE_STRING: printf("String: %s\n", value.data.string); break; default: printf("other\n"); } return 0; }
value = 42 case value in Integer => number if number > 0 puts "Positive integer: #{number}" in Integer puts "Non-positive integer" in String => text puts "String: #{text}" in [Integer, *rest] puts "Array starting with an integer" else puts "Something else" end
Ruby's case/in pattern matching (available since Ruby 3.0) enables structural dispatch on object type, value, and shape simultaneously — no type tag enum or union required. The => operator captures matched values into a variable. Guard clauses (if condition) add extra constraints. This replaces large chains of if/elsif type checks.
Array deconstruction patterns
#include <stdio.h> int main(void) { int point[] = {3, 7}; int first = point[0], second = point[1]; printf("x=%d, y=%d\n", first, second); /* Pattern match on structure: manual index checks */ if (point[0] == 0 && point[1] == 0) { printf("origin\n"); } return 0; }
point = [3, 7] case point in [0, 0] puts "origin" in [0, Integer => y] puts "on y-axis at #{y}" in [Integer => x, 0] puts "on x-axis at #{x}" in [Integer => x, Integer => y] puts "at (#{x}, #{y})" end # Deconstruct in one-line form [10, 20, 30] => [first, *rest] puts "first=#{first} rest=#{rest.inspect}"
Array patterns match both the structure (number of elements) and the type of each element simultaneously. The => operator binds a matched value to a local variable. *rest captures remaining elements. The one-line pattern match form (expression => pattern) is useful for destructuring a known structure without a full case expression.
Hash patterns
#include <stdio.h> #include <string.h> typedef struct { char type[16]; char name[64]; int age; } Record; int main(void) { Record record = { .type = "person", .name = "Alice", .age = 30 }; if (strcmp(record.type, "person") == 0 && record.age >= 18) { printf("Adult: %s\n", record.name); } return 0; }
record = { type: "person", name: "Alice", age: 30 } case record in { type: "person", name: String => name, age: (18..) => age } puts "Adult: #{name}, age #{age}" in { type: "person", name: String => name } puts "Minor: #{name}" in { type: "robot", name: String => name } puts "Robot: #{name}" end
Hash patterns match a subset of keys — extra keys are ignored by default. Values can be matched by type (String), by literal value, by range ((18..) matches 18 or greater), or captured with =>. **rest captures unmatched keys. Hash pattern matching replaces chains of key_exists && type_check && value_check.
Find pattern — search within arrays
#include <stdio.h> #include <string.h> int main(void) { const char *log_lines[] = { "INFO: starting up", "ERROR: connection failed", "INFO: retrying", }; for (int index = 0; index < 3; index++) { if (strncmp(log_lines[index], "ERROR:", 6) == 0) { printf("Found error: %s\n", log_lines[index]); break; } } return 0; }
log_lines = [ "INFO: starting up", "ERROR: connection failed", "INFO: retrying", ] case log_lines in [*, /^ERROR:/ => error_line, *] puts "Found error: #{error_line}" else puts "No errors in log" end # One-line form to extract a value numbers = [1, 2, "three", 4, 5] case numbers in [*, String => text, *] puts "Found string: #{text}" end
The find pattern ([*, pattern, *]) searches for an element matching the middle pattern anywhere in the array, with the splats capturing everything before and after. A regex works as the pattern for string elements. This replaces the manual linear scan loop needed in C. The find pattern is particularly useful for parsing mixed-type collections or log lines.