Hello World & Running
Hello, World
Ruby programs need no
#include directives, no main function, and no return 0 — top-level code executes immediately, line by line.#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}puts "Hello, World!"puts prints a value followed by a newline — the direct equivalent of printf("...\n").Running a program
Ruby is interpreted — there is no compile step, no object file, no linker.
// 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:
# irbruby 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
Ruby uses
# for single-line comments; the =begin/=end block form exists but is almost never used in practice.#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 countConsecutive
# lines are the idiomatic Ruby style for multi-line comments. The =begin/=end markers must be at the very beginning of the line.Output — puts, print, p
Ruby has three main output methods:
puts adds a newline, print does not, and p shows the value's structure for debugging.#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]p calls .inspect on its argument, showing quotes on strings and brackets on arrays — invaluable for debugging. Unlike printf, Ruby uses string interpolation (#{expression}) instead of format specifiers for most output.Variables & Types
No type declarations
Ruby variables are created on first assignment — no type declaration, no
int, no double, no char[].#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}"The runtime tracks types automatically, and 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.Dynamic typing — variables can change type
In Ruby, variables are not bound to a type — only objects are. Reassigning a variable to a different type is perfectly valid.
#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 # ArrayThe
.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
Ruby constants start with an uppercase letter — real, scoped runtime values, not C's textual
#define substitution.#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_URLSCREAMING_SNAKE_CASE is the convention for simple values, and constants respect module/class scope. Reassigning a constant produces a warning but is not a fatal error.nil — not NULL
Ruby's
nil is a real object of class NilClass — not an invalid memory address, and never a segfault.#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 # 0Calling a method that
nil does not understand raises a clean 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
Watch out: exactly two Ruby values are falsy —
nil and false. Everything else is truthy, including 0, "", and [].#include <stdio.h>
int main(void) {
/* C: 0, NULL, '\0' 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!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
Ruby supports parallel assignment: the right-hand side is fully evaluated before assignment, so a swap needs no temporary variable.
#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.inspectThe 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
In Ruby, strings are first-class objects with a rich API — 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]Operations that require
#include <string.h> and manual loops in C — converting case, checking for a substring, reversing — are single method calls. There are no null terminators, no buffer overflows, and no separately tracked lengths.String interpolation
Ruby interpolates
#{expression} inside double-quoted strings — a far more concise alternative to sprintf/snprintf, with any expression valid inside the braces.#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"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
Ruby's
String class has over 80 built-in methods: strip trims whitespace, gsub substitutes globally, split returns an array.#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).inspectAll 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)
Ruby's squiggly heredoc (
<<~) writes a multiline string directly, stripping leading whitespace so source indentation does not leak into the text.#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.chompThe terminator label is arbitrary, and
chomp removes the trailing newline that heredocs include. Unlike C's adjacent string literal trick, heredocs support interpolation by default — use a single-quoted terminator (<<~'LETTER') to disable it.String formatting with %
Ruby's
% operator applies printf-style format strings — the same specifiers as C's printf.#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" % 42For multiple arguments, pass an array:
"format" % [first, second]. sprintf and its alias format are also available. For most output, string interpolation is preferred.Chilled string literals (Ruby 4.0)
Ruby 4.0 chilled string literals rather than freezing them: mutating one still works, and says nothing about it unless deprecation warnings are on. A C string literal lives in read-only memory and writing through it is undefined behavior; Ruby's plan is to make the same write an error, and 4.0 is the step before that.
#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;
}# A literal is CHILLED in Ruby 4.0: mutating it works, and says
# nothing unless deprecation warnings are on.
greeting = "hello"
puts greeting.frozen? # false
# A string you froze yourself is a different matter:
name = "ada".freeze
begin
name << "!"
rescue FrozenError => error
puts error.message
end
mutable = +"hello" # unary + gives a mutable copy
mutable << ", world"
puts mutableWhat does raise
FrozenError today is a string you froze yourself with .freeze, which is the closest Ruby has to C's read-only literal. For a string you mean to build up, ask for a mutable one: +"hello" or String.new, both of which keep working when the default flips.Numbers
Arbitrary precision integers
Ruby has a single
Integer type with arbitrary precision — there is no integer overflow, ever.#include <stdio.h>
#include <stdint.h>
int main(void) {
/* C: fixed-width types; unsigned overflow wraps silently
(signed overflow is undefined behavior) */
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 worksNo
int, long, uint64_t, or __int128 needed; the runtime automatically promotes to a big-integer representation. Underscores are allowed in numeric literals for readability: 1_000_000.Integer methods
Ruby integers are objects with a rich method API — predicates like
even? and looping methods like times live right on the number.#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 }Predicate methods ending in
? return true or false: even?, odd?, positive?, negative?, zero?. The times method replaces C's counting for loop, and upto/downto handle ranges.Float — one type for all real numbers
Ruby has a single
Float class — IEEE 754 double precision, the same as C's double — with no float-versus-double distinction.#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)Math functions live in the
Math module: Math.sqrt, Math.sin, Math::PI. Floating-point division by zero produces Float::INFINITY — as it does in C on any machine with IEEE 754 floating point, which is every one this code is likely to meet.Integer division and modulo
Watch out: Ruby's integer
/ is floor division (rounds toward negative infinity), while C99 truncates toward zero — the results differ for negative operands.#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-7 / 2 is -4 in Ruby but -3 in C — a common source of bugs when porting C logic. Ruby's % 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; no pow() call needed.Numeric conversions
Ruby's conversion methods follow one consistent naming convention:
to_i, to_f, to_s, to_r — replacing atoi, strtol, strtod, and snprintf.#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"to_i accepts a radix argument: "0xFF".to_i(16) parses hexadecimal, and 255.to_s(16) formats it. No headers to include.Arrays
Creating arrays — dynamic, not fixed-size
Ruby arrays are dynamically sized and heterogeneous — one array can hold integers, strings, symbols, and
nil simultaneously.#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.classThere 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
Ruby arrays support negative indexing —
array[-1] is the last element — and slicing by range or start/length.#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.lastNegative indexing eliminates the common C pattern of
array[length - 1]. Slicing returns a new array with no manual allocation or copy, and an out-of-bounds access returns nil rather than undefined behavior.push, pop, shift, unshift
Ruby arrays grow and shrink automatically — no
realloc, no capacity management, no free.#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]push (or <<) appends; pop removes from the end; unshift prepends; shift removes from the front. These map to the standard stack and queue operations without any memory management ceremony.map, select, reject
Every Ruby array carries functional collection operations:
map transforms, select keeps matches, reject keeps non-matches.#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.inspectThese come from the
Enumerable module. No temporary arrays to allocate, no index management, no explicit loops.reduce / inject — fold to a single value
reduce (alias: inject) folds an array into a single value by applying a block to an accumulator and each element in turn.#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 shorthandThe first argument is the initial accumulator value. The symbol shorthand
inject(:*) is equivalent to a block that applies *. sum is a built-in convenience method for the common addition case.sort, uniq, flatten, compact
Common array cleanups are one-word methods:
sort, uniq, flatten, compact — no comparator function pointer, no manual passes.#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 valuessort returns a sorted copy using the <=> spaceship operator. uniq removes duplicates preserving order; flatten recursively expands nested arrays (no C equivalent at all); compact removes nil values. Methods ending in ! mutate in place: sort!, uniq!.Hashes
Creating hashes — no C stdlib equivalent
Ruby has a built-in
Hash class — a key-value store with O(1) average lookup that C leaves to third-party libraries or parallel arrays.#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"]The
{ key: value } syntax uses symbol keys (the most common style); { "key" => value } accepts any object as a key. Hashes maintain insertion order (since Ruby 1.9).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 }.inspecteach 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.inspectHash.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.sumRuby 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).inspectRanges 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 gradeRanges 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"
endRuby'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 authenticatedunless 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 > 100Ruby 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
endwhile loops run while the condition is true; until runs while it is false (equivalent to while !condition). The post-test loop is spelled begin … end while condition — the body runs once before the condition is read — but it is rare, and idiomatic Ruby writes 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"
endRuby'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
endloop 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 matterRuby 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).inspectA
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.maxThe
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 resultEach 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 accountRuby 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.idClass methods are defined with
self.method_name — the nearest C has is a free function named after the struct it serves, since a C struct cannot hold functions of its own. 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.factorialRuby 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
#define _XOPEN_SOURCE 700 /* M_PI is POSIX, not C17 */
#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").greetinclude 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.nameIncluding
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}"
endRuby 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())"
endrescue 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"
endCustom 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}"
endretry 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 is a compile-time promise about one name, not a lock */
const int value = 42;
/* value = 43; // compile error */
const char *str = "hello";
/* str[0] = 'H'; // compile error too: str points at const */
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 stringCalling
.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. Note that Ruby 4.0 string literals are only chilled, not frozen — mutating one works rather than raising — so this is about objects you froze yourself. 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"
endRuby'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}"
endHash 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}"
endThe 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.