PONY λ M2 Modula-2

Ruby.CodeCompared.To/D

An interactive executable cheatsheet for Rubyists learning D

Ruby 4.0 D 2.112
Output & Basics
Hello World
puts "Hello, World!"
import std.stdio; void main() { writeln("Hello, World!"); }

Every D program needs a main() function as the entry point and must import std.stdio to use writeln. Unlike Ruby, D is a compiled, statically-typed language — there is no top-level script mode. The import statement is roughly equivalent to Ruby's require.

write, writeln, writef
print "no newline" puts "with newline" printf("%d items ", 5)
import std.stdio; void main() { write("no newline "); writeln("with newline"); writefln("%d items", 5); }

D's output family mirrors Ruby's: write() omits the newline (like Ruby's print), writeln() appends one (like puts), and writefln() formats like C's printf but adds a newline. The f variants use C-style format specifiers: %d, %s, %f, %.2f.

String formatting (no interpolation)
name = "Alice" age = 30 puts "#{name} is #{age} years old"
import std.stdio; import std.format; void main() { string name = "Alice"; int age = 30; writefln("%s is %d years old", name, age); string message = format("%s is %d years old", name, age); writeln(message); }

D does not have Ruby-style string interpolation (#{}). Instead, use writefln() for direct formatted output or format() from std.format to build a formatted string. D 2.x does have experimental string interpolation via i"..." literals, but it is not yet stable — format() is the idiomatic approach.

Comments
# Single-line comment x = 42 # inline comment =begin Multi-line comment =end puts x
import std.stdio; void main() { // Single-line comment int x = 42; // inline comment /* Multi-line comment */ /+ Nestable /+ block +/ comment +/ writeln(x); }

D has three comment styles: // for single-line (like Ruby's #), /* ... */ for block comments (like most C-family languages), and the unique /+ ... +/ style which supports nesting. Nested comments are useful for commenting out large code blocks that already contain comments — something /* */ cannot do.

Printing multiple values
x = 10 y = 20 puts "#{x} + #{y} = #{x + y}" p [x, y]
import std.stdio; void main() { int x = 10; int y = 20; writeln(x, " + ", y, " = ", x + y); writeln([x, y]); }

D's writeln() accepts any number of arguments and prints them all consecutively without separators. Passing an array prints it in [1, 2] notation. This is more flexible than Ruby's puts, which calls .to_s on one argument and prints each array element on its own line.

Variables & Types
Type inference with auto
x = 42 greeting = "hello" ratio = 3.14 puts x.class puts greeting.class
import std.stdio; void main() { auto x = 42; auto greeting = "hello"; auto ratio = 3.14; writeln(typeof(x).stringof); writeln(typeof(greeting).stringof); writeln(typeof(ratio).stringof); }

D's auto keyword infers the variable's type at compile time from the right-hand side — similar in feel to Ruby's implicit typing, but resolved at compile time rather than at runtime. The typeof() intrinsic returns the compile-time type; .stringof gives its name as a string. Once assigned, an auto variable has a fixed type that cannot change.

Explicit type annotations
# Ruby has no runtime-enforced type annotations count = 0 count += 1 puts count
import std.stdio; void main() { int count = 0; count += 1; long bigNumber = 10_000_000_000L; double ratio = 3.14159; bool flag = true; writeln(count, " ", bigNumber, " ", ratio, " ", flag); }

D's core integer types are int (32-bit), long (64-bit), uint, ulong, and smaller variants. Floating-point types are float (32-bit), double (64-bit), and real (80-bit on x86). Numeric literals accept underscores as separators — the same convention Ruby uses.

immutable vs const
# Ruby: uppercase names are constants (convention, not enforced) MAX_SIZE = 100 GREETING = "hello".freeze puts MAX_SIZE
import std.stdio; void main() { immutable int maxSize = 100; immutable string greeting = "hello"; const double pi = 3.14159; writeln(maxSize, " ", greeting, " ", pi); }

immutable means the value can never change and is safe to share across threads without locking — stronger than Ruby's freeze. const means the current variable cannot modify the value, but something else might (like a non-const alias). In practice, prefer immutable for true constants. Note that D's string type is immutable(char)[] — strings are immutable by default, which matches Ruby 4.0's frozen string behavior.

Type inspection and checking
puts 42.class puts 3.14.is_a?(Float) puts "hello".is_a?(String)
import std.stdio; void main() { auto x = 42; writeln(typeof(x).stringof); writeln(is(typeof(x) == int)); writeln(is(typeof(3.14) == double)); writeln(is(typeof("hello") == string)); }

D's typeof(expr) returns the compile-time type of an expression, and is(T == U) checks type equality at compile time. These are compile-time constructs, not runtime reflection — the check happens during compilation, not execution. For runtime type checking on class hierarchies, D uses cast(T) which returns null on failure.

Nullable types
result = nil puts result.nil? result = 42 puts result
import std.stdio; import std.typecons; void main() { Nullable!int result; writeln(result.isNull); result = 42; writeln(result.isNull); writeln(result.get); }

D's primitive types (int, double, etc.) cannot be null — they always have a value. Nullable!T from std.typecons wraps any type to add an optional absent state, similar to Haskell's Maybe or Rust's Option. This is more explicit than Ruby where any variable can be nil. D class references can be null, following C++ conventions.

Strings
Strings are immutable slices
greeting = "Hello, World!" puts greeting.length puts greeting.class
import std.stdio; void main() { string greeting = "Hello, World!"; writeln(greeting.length); writeln(typeof(greeting).stringof); }

D's string type is an alias for immutable(char)[] — a slice of immutable UTF-8 bytes. The .length property returns the byte count, not the Unicode character count (the same distinction Ruby draws between .bytesize and .length). Because strings are immutable slices, string operations typically return new slices rather than modifying in place.

String slicing
word = "hello" puts word[0] puts word[1..3] puts word[-1]
import std.stdio; void main() { string word = "hello"; writeln(word[0]); writeln(word[1..4]); writeln(word[$-1]); }

D uses 0-based indexing like Ruby. Slices use start..end where the end is exclusive (not inclusive like Ruby's 1..3). The $ symbol inside index brackets refers to the length of the array or string — so word[$-1] is the last character, equivalent to Ruby's word[-1].

String concatenation and append
greeting = "Hello" greeting += ", World!" words = ["one", "two", "three"] puts words.join(", ")
import std.stdio; import std.array; void main() { string greeting = "Hello"; greeting ~= ", World!"; writeln(greeting); string[] words = ["one", "two", "three"]; writeln(words.join(", ")); }

D uses the ~ operator for concatenation and ~= for append-in-place — analogous to Ruby's + and <<. The tilde is D's dedicated concatenation operator, keeping + strictly for numeric addition. join() from std.array works identically to Ruby's Array#join.

Common string operations
message = "Hello, World!" puts message.upcase puts message.downcase puts message.include?("World") puts message.gsub("World", "D")
import std.stdio; import std.string; void main() { string message = "Hello, World!"; writeln(message.toUpper); writeln(message.toLower); writeln(message.indexOf("World") >= 0); writeln(message.replace("World", "D")); }

std.string provides the most common string operations. Note that UFCS (Universal Function Call Syntax) lets you write message.toUpper instead of toUpper(message) — making free functions feel like methods, similar to Ruby's method call style. indexOf() returns -1 when not found, so checking >= 0 replaces Ruby's .include?.

Split and strip
csv = "apple,banana,cherry" fruits = csv.split(",") puts fruits.inspect puts " hello ".strip
import std.stdio; import std.string; void main() { string csv = "apple,banana,cherry"; string[] fruits = csv.split(","); writeln(fruits); writeln(" hello ".strip); }

split() and strip() from std.string are direct equivalents of Ruby's .split and .strip. D's split() returns a string[] (dynamic array of strings). UFCS makes the call chain readable: " hello ".strip is strip(" hello ") written in method-call style.

Type conversion with strings
puts 42.to_s puts "123".to_i puts "3.14".to_f
import std.stdio; import std.conv; void main() { writeln(to!string(42)); writeln(to!int("123")); writeln(to!double("3.14")); writeln(42.to!string); }

std.conv.to!(T) is D's universal conversion function — it converts between any two compatible types, including string-to-numeric and numeric-to-string. The ! is D's template argument syntax: to!int("123") means "call to with type parameter int". UFCS lets you write this as "123".to!int, which reads exactly like Ruby's "123".to_i.

Arrays & Slices
Array creation
numbers = [1, 2, 3, 4, 5] words = ["apple", "banana", "cherry"] puts numbers.inspect puts numbers.class
import std.stdio; void main() { int[] numbers = [1, 2, 3, 4, 5]; string[] words = ["apple", "banana", "cherry"]; writeln(numbers); writeln(typeof(numbers).stringof); }

D dynamic arrays (T[]) use the same [...] literal syntax as Ruby. Unlike Ruby arrays, D arrays are homogeneous — all elements must be the same type. The type is inferred from the elements if you use auto. Dynamic arrays in D are reference types backed by a garbage-collected heap, not value types — assigning one to another creates a reference, not a copy.

Append and concatenate
numbers = [1, 2, 3] numbers << 4 numbers.push(5) combined = numbers + [6, 7] puts combined.inspect
import std.stdio; void main() { int[] numbers = [1, 2, 3]; numbers ~= 4; numbers ~= 5; int[] combined = numbers ~ [6, 7]; writeln(combined); }

D uses ~= to append to an array (like Ruby's << or .push) and ~ to concatenate two arrays (like Ruby's +). The ~ operator is consistent: it concatenates arrays just as it concatenates strings. There is no separate push method — ~= is the idiomatic way to append a single element.

Slicing arrays
numbers = [10, 20, 30, 40, 50] puts numbers[1..3].inspect puts numbers[0, 3].inspect puts numbers[-2..].inspect
import std.stdio; void main() { int[] numbers = [10, 20, 30, 40, 50]; writeln(numbers[1..4]); writeln(numbers[0..3]); writeln(numbers[$-2..$]); }

D slice syntax array[start..end] is end-exclusive (Python-style), so numbers[1..4] gives elements at indices 1, 2, and 3 — equivalent to Ruby's numbers[1..3]. Slices are zero-copy views into the original array — they share the same memory. Modifying a slice element also modifies the original unless you call .dup first.

Array length, sort, reverse
numbers = [3, 1, 4, 1, 5, 9, 2, 6] puts numbers.length puts numbers.sort.inspect puts numbers.reverse.inspect puts numbers.sum
import std.stdio; import std.algorithm; void main() { int[] numbers = [3, 1, 4, 1, 5, 9, 2, 6]; writeln(numbers.length); writeln(numbers.dup.sort); writeln(numbers.dup.sort.reverse); writeln(numbers.sum); }

sort() from std.algorithm sorts in place and requires a mutable (non-slice) array — .dup creates a copy first. sum() works directly on any numeric range. UFCS makes these read like Ruby method calls. Note that sort in D returns a special SortedRange type which reverse can then operate on.

Static arrays (fixed size)
# Ruby has no fixed-size arrays — all arrays are dynamic board = Array.new(3) { Array.new(3, 0) } puts board.inspect
import std.stdio; void main() { int[5] fixed = [1, 2, 3, 4, 5]; writeln(fixed); writeln(fixed.sizeof); int[3][3] grid; grid[1][1] = 1; writeln(grid); }

D's T[N] is a static array — size is fixed at compile time and the data lives on the stack, not the heap. sizeof returns the byte size (here, 5 × 4 bytes = 20). Static arrays are value types — assigning one to another copies all the data. They are more efficient than dynamic arrays for small, fixed-size collections, unlike Ruby which has only one array type.

Ranges & Algorithms
Integer ranges with iota
puts (1..5).to_a.inspect puts (0...10).to_a.inspect puts (1..10).step(2).to_a.inspect
import std.stdio; import std.range; import std.array; void main() { writeln(iota(1, 6).array); writeln(iota(0, 10).array); writeln(iota(1, 11, 2).array); }

iota(start, end) from std.range generates a lazy integer range (end-exclusive), equivalent to Ruby's (start...end).to_a. The three-argument form iota(start, end, step) mirrors Ruby's .step(). .array materializes the lazy range into a concrete int[] — equivalent to Ruby's .to_a. Most functions accept lazy ranges directly without materializing.

map — transform elements
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } puts doubled.inspect
import std.stdio; import std.algorithm; import std.array; void main() { int[] numbers = [1, 2, 3, 4, 5]; auto doubled = numbers.map!(n => n * 2).array; writeln(doubled); }

map! from std.algorithm transforms each element — identical in purpose to Ruby's .map. The ! after map is D's template instantiation syntax (not "bang" or mutation — D mutation methods use no special naming). UFCS lets you write numbers.map!(n => n * 2) even though map is a free function in std.algorithm, not a method on arrays.

filter — select elements
numbers = [1, 2, 3, 4, 5, 6, 7, 8] evens = numbers.select { |n| n.even? } puts evens.inspect
import std.stdio; import std.algorithm; import std.array; void main() { int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8]; auto evens = numbers.filter!(n => n % 2 == 0).array; writeln(evens); }

filter! is D's equivalent of Ruby's .select — it keeps elements for which the predicate returns true. Like all std.algorithm functions, it returns a lazy range, so the actual filtering is deferred until you iterate or call .array. This lazy evaluation is a performance advantage: chaining filter! and map! makes a single pass, not two separate allocations.

reduce — accumulate
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |sum, n| sum + n } product = numbers.reduce(1, :*) puts total puts product
import std.stdio; import std.algorithm; void main() { int[] numbers = [1, 2, 3, 4, 5]; auto total = numbers.reduce!((sum, n) => sum + n); auto product = numbers.reduce!((acc, n) => acc * n); writeln(total); writeln(product); }

reduce! from std.algorithm folds a range left, equivalent to Ruby's .reduce or .inject. The accumulator starts as the first element if no seed is given. D also provides fold! which accepts an explicit seed: fold!((sum, n) => sum + n)(numbers, 0). For simple operations, sum(numbers) and numbers.minElement / numbers.maxElement are more direct.

Chaining algorithms (pipeline)
result = (1..10) .select { |n| n.odd? } .map { |n| n ** 2 } .reject { |n| n > 50 } puts result.inspect
import std.stdio; import std.range; import std.algorithm; import std.array; void main() { auto result = iota(1, 11) .filter!(n => n % 2 != 0) .map!(n => n * n) .filter!(n => n <= 50) .array; writeln(result); }

D's range pipeline composes lazily, just like Ruby's Enumerator chain. The chain filter! → map! → filter! makes a single pass through the data — no intermediate arrays are allocated until .array is called at the end. This is equivalent to Ruby's lazy enumerator (.lazy.select.map.first(n)) but is the default behavior in D, not an opt-in.

sort, find, any, all
words = ["banana", "apple", "cherry"] puts words.sort.inspect puts words.include?("apple") puts words.any? { |w| w.length > 5 } puts words.all? { |w| w.length > 3 }
import std.stdio; import std.algorithm; import std.array; void main() { string[] words = ["banana", "apple", "cherry"]; writeln(words.dup.sort.array); writeln(!words.find("apple").empty); writeln(words.any!(w => w.length > 5)); writeln(words.all!(w => w.length > 3)); }

find() returns a range starting at the found element (or empty if not found); checking .empty is the idiomatic "exists" test. any! and all! correspond directly to Ruby's .any? and .all?. All of these live in std.algorithm and accept lazy ranges as well as concrete arrays.

zip and enumerate
fruits = ["apple", "banana", "cherry"] prices = [1.20, 0.50, 2.00] fruits.zip(prices).each { |fruit, price| puts "#{fruit}: #{price}" } fruits.each_with_index { |fruit, i| puts "#{i}: #{fruit}" }
import std.stdio; import std.range; import std.algorithm; void main() { string[] fruits = ["apple", "banana", "cherry"]; double[] prices = [1.20, 0.50, 2.00]; foreach (pair; fruits.zip(prices)) writefln("%s: %.2f", pair[0], pair[1]); foreach (index, fruit; fruits) writefln("%d: %s", index, fruit); }

zip() from std.range pairs elements from two or more ranges, equivalent to Ruby's .zip. D's foreach supports index-and-value iteration directly — foreach (index, value; collection) is the idiomatic equivalent of Ruby's .each_with_index, with no need for a separate enumerate() call.

Associative Arrays
Associative array creation
person = { name: "Alice", city: "Portland" } puts person[:name] puts person.class
import std.stdio; void main() { string[string] person = ["name": "Alice", "city": "Portland"]; writeln(person["name"]); writeln(typeof(person).stringof); }

D's associative arrays use the type syntax ValueType[KeyType] — note the reversed order compared to what most languages use. The literal syntax ["key": value] uses a colon separator like Ruby's "key" => value hash rockets. Unlike Ruby, D's associative array type is homogeneous — all keys and all values must be the same type.

Access, membership, default
scores = { alice: 95, bob: 87 } puts scores[:alice] puts scores.key?(:charlie) puts scores.fetch(:charlie, 0)
import std.stdio; void main() { int[string] scores = ["alice": 95, "bob": 87]; writeln(scores["alice"]); writeln("charlie" in scores); writeln(scores.get("charlie", 0)); }

The in operator returns a pointer to the value if the key exists, or null if not — truthy/falsy like Ruby's .key? but with direct value access. .get(key, default) is equivalent to Ruby's Hash#fetch(key, default). Accessing a missing key with [] throws a RangeError in D, unlike Ruby which returns nil.

Iterating associative arrays
capitals = { france: "Paris", japan: "Tokyo" } capitals.each { |country, city| puts "#{country}: #{city}" } puts capitals.keys.inspect puts capitals.values.inspect
import std.stdio; void main() { string[string] capitals = ["france": "Paris", "japan": "Tokyo"]; foreach (country, city; capitals) writefln("%s: %s", country, city); writeln(capitals.keys); writeln(capitals.values); }

D's foreach (key, value; assocArray) destructures key-value pairs directly — identical in structure to Ruby's .each do |key, value|. The .keys and .values properties return dynamic arrays. Iteration order is not guaranteed (same as Ruby's Hash before 1.9, and unlike Ruby's current insertion-ordered hash).

Add, update, remove
inventory = { apples: 5, bananas: 3 } inventory[:oranges] = 10 inventory[:apples] += 2 inventory.delete(:bananas) puts inventory.inspect
import std.stdio; void main() { int[string] inventory = ["apples": 5, "bananas": 3]; inventory["oranges"] = 10; inventory["apples"] += 2; inventory.remove("bananas"); writeln(inventory); }

Assigning to a new key inserts it; assigning to an existing key updates it — identical to Ruby. .remove(key) deletes a key and returns true if it was present, false if not. There is no delete method (Ruby's name); D uses remove consistently for associative array deletion.

Control Flow
if / else if / else
temperature = 22 if temperature > 30 puts "hot" elsif temperature > 20 puts "warm" else puts "cool" end
import std.stdio; void main() { int temperature = 22; if (temperature > 30) writeln("hot"); else if (temperature > 20) writeln("warm"); else writeln("cool"); }

D uses else if (two words with a space) where Ruby uses elsif. Parentheses around the condition are required in D (unlike Ruby). Single-statement branches don't need braces — though style guides recommend always using them. D has no unless keyword; use if (!condition) instead.

foreach over collections
fruits = ["apple", "banana", "cherry"] fruits.each do |fruit| puts fruit end (1..5).each { |i| puts i }
import std.stdio; void main() { string[] fruits = ["apple", "banana", "cherry"]; foreach (fruit; fruits) writeln(fruit); foreach (i; 1..6) writeln(i); }

D's foreach (item; collection) is the idiomatic loop — equivalent to Ruby's .each block. For integer ranges, foreach (i; start..end) iterates from start to end-1 (end-exclusive). Unlike Ruby's .each, there is no functional-style return value — foreach is a statement, not an expression.

while loop
countdown = 5 while countdown > 0 puts countdown countdown -= 1 end puts "Blastoff!"
import std.stdio; void main() { int countdown = 5; while (countdown > 0) { writeln(countdown); countdown -= 1; } writeln("Blastoff!"); }

D's while is syntactically identical to C — condition in parentheses, body in braces. Unlike Julia (which had a soft-scope issue), D's while loop body shares the enclosing function scope, so modifying countdown inside the loop modifies the outer variable as expected. D also has a do { } while (condition); form that tests the condition after the first iteration.

switch / final switch
grade = "B" case grade when "A" then puts "excellent" when "B" then puts "good" when "C" then puts "average" else puts "other" end
import std.stdio; void main() { string grade = "B"; switch (grade) { case "A": writeln("excellent"); break; case "B": writeln("good"); break; case "C": writeln("average"); break; default: writeln("other"); break; } }

D's switch requires explicit break to prevent fall-through, unlike Ruby's case/when which never falls through. D also provides final switch for enum types: the compiler checks that every enum value is handled, producing an error rather than a silent default. This is equivalent to exhaustiveness checking in pattern matching languages.

break and continue
(1..10).each do |i| next if i % 2 == 0 break if i > 7 puts i end
import std.stdio; void main() { foreach (i; 1..11) { if (i % 2 == 0) continue; if (i > 7) break; writeln(i); } }

D uses continue (not next like Ruby) to skip to the next iteration, and break to exit the loop — following C and most other languages' convention. D also supports labeled break and continue for nested loops: outer: foreach ... { foreach ... { break outer; } } — equivalent to Ruby's break with a label.

Ternary operator
score = 75 result = score >= 60 ? "pass" : "fail" puts result
import std.stdio; void main() { int score = 75; string result = score >= 60 ? "pass" : "fail"; writeln(result); }

The ternary operator is identical in D and Ruby: condition ? value_if_true : value_if_false. This is one of the few pieces of syntax shared exactly between the two languages. In D, the ternary is an expression (returns a value), not a statement — same as Ruby, and unlike some languages that restrict it.

Functions
Basic function definition
def greet(name) "Hello, #{name}!" end puts greet("Alice")
import std.stdio; string greet(string name) { return "Hello, " ~ name ~ "!"; } void main() { writeln(greet("Alice")); }

D functions live at module scope (outside main()), declared with return type, name, and typed parameters. The return type is required — there is no implicit typing of function signatures. void means no return value. Unlike Ruby, D's implicit last-expression return does not apply to functions; you must write return explicitly (unless using lambda assignment form for trivial functions).

Auto return type inference
def add(x, y) = x + y def square(x) = x ** 2 puts add(3, 4) puts square(5)
import std.stdio; auto add(int x, int y) { return x + y; } auto square(double x) { return x * x; } void main() { writeln(add(3, 4)); writeln(square(5.0)); }

Using auto as the return type tells the compiler to infer it from the return statement. This is similar in spirit to Ruby 4.0's one-liner method syntax, though still requires a function body. The compiler verifies that all return paths return the same type. auto return types work at the function level but are restricted in some contexts (virtual functions, for example, must have explicit return types).

Default arguments
def greet(name, greeting = "Hello") puts "#{greeting}, #{name}!" end greet("Alice") greet("Bob", "Hi")
import std.stdio; void greet(string name, string greeting = "Hello") { writefln("%s, %s!", greeting, name); } void main() { greet("Alice"); greet("Bob", "Hi"); }

Default argument values work identically in D and Ruby — declare them in the function signature with = value. Default parameters must come after all required parameters. Unlike Python and Ruby, D does not support calling with named arguments by default — arguments are positional only (unless using keyword argument syntax available in some metaprogramming contexts).

Pure functions and @safe
# Ruby has no built-in purity annotations def double(x) = x * 2 puts double(21)
import std.stdio; pure int double_it(int x) { return x * 2; } @safe pure int triple(int x) { return x * 3; } void main() { writeln(double_it(21)); writeln(triple(14)); }

pure declares that a function only depends on its arguments and has no side effects — the compiler can optimize calls aggressively and the function is safe to run at compile time (CTFE). @safe restricts the function from using unsafe features (pointer arithmetic, casting, @system code). These are opt-in correctness guarantees that D provides but Ruby has no equivalent for.

Variadic functions
def sum(*numbers) numbers.sum end puts sum(1, 2, 3, 4, 5)
import std.stdio; int sum(int[] numbers...) { int total = 0; foreach (n; numbers) total += n; return total; } void main() { writeln(sum(1, 2, 3, 4, 5)); }

D's typed variadic parameter T[] name... collects extra arguments into a typed array — equivalent to Ruby's splat (*args). The arguments must all be the same type. D also has untyped C-style variadics (... with no type) for interoperating with C, but the typed form is idiomatic D and provides full type safety.

Universal Function Call Syntax
UFCS: free functions as methods
# Ruby: methods live on objects "hello".upcase [1, 2, 3].length 42.to_s
import std.stdio; import std.string; import std.conv; void main() { writeln("hello".toUpper); writeln([1, 2, 3].length); writeln(42.to!string); writeln("hello".toUpper.toLower.strip); }

Universal Function Call Syntax (UFCS) is D's most Ruby-like feature: any free function f(a, b) can be called as a.f(b). This means std.string.toUpper(str) and str.toUpper are identical. UFCS enables fluent method-chaining on any type without opening or monkey-patching a class — a powerful but less chaotic version of Ruby's open-class extension.

UFCS method chaining with algorithms
result = (1..10) .select(&:odd?) .map { |n| n ** 2 } .sum puts result
import std.stdio; import std.range; import std.algorithm; void main() { auto result = iota(1, 11) .filter!(n => n % 2 != 0) .map!(n => n * n) .sum; writeln(result); }

UFCS makes D's std.algorithm feel like Ruby's Enumerable — the data flows left to right through a chain of transformations. This is intentional: D's standard library is designed for UFCS chaining. The entire pipeline is lazy and evaluated in a single pass when .sum consumes it — more efficient than Ruby's eager intermediate arrays.

Extending types with free functions
# Ruby uses open classes / refinements: class Integer def factorial self <= 1 ? 1 : self * (self - 1).factorial end end puts 5.factorial
import std.stdio; long factorial(long n) pure { return n <= 1 ? 1 : n * factorial(n - 1); } bool isPrime(int n) pure { if (n < 2) return false; foreach (i; 2..n) if (n % i == 0) return false; return true; } void main() { writeln(5.factorial); writeln(7.isPrime); writeln(10.isPrime); }

UFCS lets you "add methods" to any type by writing a free function whose first parameter is that type. This is safer than Ruby's open classes: the function is only available in modules that import it, and it cannot accidentally override existing behavior. 5.factorial calls factorial(5) — no class reopening, no module_function, no refine.

Closures & Delegates
Lambda expressions
square = ->(x) { x ** 2 } double = ->(x) { x * 2 } puts square.call(5) puts double.(4) puts [1, 2, 3].map(&square).inspect
import std.stdio; import std.algorithm; import std.array; void main() { auto square = (double x) => x * x; auto double_it = (int x) => x * 2; writeln(square(5.0)); writeln(double_it(4)); writeln([1, 2, 3].map!(n => n * 2).array); }

D's lambda syntax (params) => expression is concise and directly analogous to Ruby's ->(x) { }. For multi-statement lambdas, use (params) { body; return value; }. Lambdas are called with regular parentheses — no .call needed. Inline lambdas inside map! do not need to capture outer variables.

Closures (captured context)
multiplier = 3 triple = ->(x) { x * multiplier } puts triple.call(7) counter = 0 increment = -> { counter += 1; counter } puts increment.call puts increment.call
import std.stdio; void main() { int multiplier = 3; auto triple = (int x) => x * multiplier; writeln(triple(7)); int counter = 0; auto increment = () { counter += 1; return counter; }; writeln(increment()); writeln(increment()); }

D closures capture outer variables by reference — modifying counter inside the closure modifies the outer counter. This is identical to Ruby's closure behavior with blocks and lambdas. The closure type in D is called a delegate — it bundles a function pointer with a context pointer (the captured environment).

Higher-order functions
def apply_twice(func, value) func.call(func.call(value)) end double = ->(x) { x * 2 } puts apply_twice(double, 3)
import std.stdio; int applyTwice(int function(int) operation, int value) { return operation(operation(value)); } void main() { auto doubleIt = (int x) => x * 2; writeln(applyTwice(doubleIt, 3)); // 12 writeln(applyTwice((int x) => x + 10, 5)); // 25 }

D distinguishes function pointers (no captures) from delegates (closure with captured context). A lambda that captures no outer variables compiles to a function pointer; one that captures local state is a delegate. Ruby's blocks and procs don't have this distinction — they always carry their closure.

scope(exit) — guaranteed cleanup
def with_resource puts "opening resource" begin yield ensure puts "closing resource (always)" end end with_resource { puts "using resource" }
import std.stdio; void processFile() { writeln("opening resource"); scope(exit) writeln("closing resource (always)"); scope(failure) writeln("cleanup on error"); scope(success) writeln("success path"); writeln("using resource"); } void main() { processFile(); }

scope(exit) runs at the end of the current scope regardless of success or failure — like Ruby's ensure. scope(failure) runs only on exception; scope(success) only on normal exit. These are placed at the resource acquisition site (near the open), not wrapped around usage — which makes the code read in acquisition order rather than requiring nesting. This is unique to D.

Structs
Struct definition
Point = Struct.new(:x, :y) origin = Point.new(0.0, 0.0) puts origin.x puts origin
import std.stdio; struct Point { double x; double y; } void main() { auto origin = Point(0.0, 0.0); writeln(origin.x); writeln(origin); }

D's struct is a value type (stored on the stack) — assigning it to another variable copies all the data. This contrasts with Ruby's Struct, which is a reference type (class instance). D structs support member functions, constructors, operator overloading, and all OOP features except inheritance — they are not "plain C structs".

Struct methods
Rectangle = Struct.new(:width, :height) do def area = width * height def perimeter = 2 * (width + height) end box = Rectangle.new(4.0, 5.0) puts box.area puts box.perimeter
import std.stdio; struct Rectangle { double width; double height; double area() { return width * height; } double perimeter() { return 2 * (width + height); } } void main() { auto box = Rectangle(4.0, 5.0); writeln(box.area); writeln(box.perimeter); }

D structs can contain method definitions directly — unlike Julia where methods live outside types. The this pointer (implicit, like Ruby's self) gives access to fields. Methods on structs can also be defined externally as free functions and called via UFCS — both styles are idiomatic.

Custom constructors
class Circle attr_reader :radius def initialize(radius) raise ArgumentError if radius <= 0 @radius = radius.to_f end def area = Math::PI * @radius ** 2 end puts Circle.new(3).area.round(4)
import std.stdio; import std.math; struct Circle { double radius; this(double r) { assert(r > 0, "radius must be positive"); radius = r; } double area() { return PI * radius ^^ 2; } } void main() { auto circle = Circle(3.0); writefln("%.4f", circle.area); }

D struct constructors use this(params) (called in Ruby as initialize). The built-in assert validates preconditions and throws an AssertError in debug builds. D uses ^^ for exponentiation (not ** or ^). The constant PI is available in std.math.

Operator overloading
Point = Struct.new(:x, :y) do def +(other) = Point.new(x + other.x, y + other.y) def to_s = "(#{x}, #{y})" end a = Point.new(1.0, 2.0) b = Point.new(3.0, 4.0) puts a + b
import std.stdio; struct Vector2 { double x, y; Vector2 opBinary(string op)(Vector2 other) if (op == "+") { return Vector2(x + other.x, y + other.y); } string toString() const { import std.format; return format("(%.1f, %.1f)", x, y); } } void main() { auto a = Vector2(1.0, 2.0); auto b = Vector2(3.0, 4.0); writeln(a + b); }

D overloads operators through specially named methods: opBinary!"+" for +, opUnary!"-" for unary minus, opIndex for [], and others. The string template parameter selects which operator to handle. toString() is called automatically by writeln, equivalent to Ruby's to_s.

Classes & OOP
Class definition
class Animal def initialize(name) @name = name end def speak puts "#{@name} says ..." end end Animal.new("Rex").speak
import std.stdio; class Animal { string name; this(string name) { this.name = name; } void speak() { writefln("%s says ...", name); } } void main() { auto animal = new Animal("Rex"); animal.speak(); }

D classes are reference types allocated on the heap with new — Ruby's model exactly. The constructor is this() (not initialize). Fields declared in the class body are public by default, unlike Ruby's instance variables which are always private. D uses this.field to disambiguate parameter names from field names, equivalent to Ruby's @name vs name.

Inheritance and override
class Animal def initialize(name) = @name = name def speak = puts "..." end class Dog < Animal def speak = puts "#{@name} says Woof!" end Dog.new("Rex").speak
import std.stdio; class Animal { string name; this(string name) { this.name = name; } void speak() { writeln("..."); } } class Dog : Animal { this(string name) { super(name); } override void speak() { writefln("%s says Woof!", name); } } void main() { Animal dog = new Dog("Rex"); dog.speak(); }

D uses : for inheritance (where Ruby uses <). The override keyword is mandatory when overriding a parent method — unlike Ruby, where silently redefining a method is always allowed. This prevents accidental overrides: if the parent class removes or renames a method, the compiler flags any override that no longer overrides anything.

Access control
class BankAccount def initialize(balance) @balance = balance end def deposit(amount) @balance += amount end private def secret = "hidden" end
import std.stdio; class BankAccount { private double balance; this(double initial) { balance = initial; } void deposit(double amount) { balance += amount; } double getBalance() { return balance; } private void secret() { writeln("hidden"); } } void main() { auto account = new BankAccount(100.0); account.deposit(50.0); writeln(account.getBalance()); }

D uses private, protected, and public access modifiers on individual members (not a section keyword like Ruby's private). By default, class members in D are public. Unlike Ruby's attr_reader/attr_writer, D has no built-in property syntax — getter and setter methods are written manually, though the @property attribute can mark them for special calling conventions.

final and abstract
module Printable def print_info raise NotImplementedError end end class Document include Printable def print_info = puts "Document: #{self.class}" end Document.new.print_info
import std.stdio; abstract class Shape { abstract double area(); void describe() { writefln("Area: %.2f", area()); } } class Square : Shape { double side; this(double side) { this.side = side; } override double area() { return side * side; } } void main() { Shape shape = new Square(4.0); shape.describe(); }

abstract class cannot be instantiated — only its subclasses can. abstract methods must be overridden by every non-abstract subclass; the compiler enforces this. final class prevents subclassing entirely. final on a method prevents overriding in subclasses. These are compile-time guarantees that Ruby's conventions (not enforced) can only approximate.

Interfaces & Mixins
Interfaces
module Drawable def draw = raise NotImplementedError def shape_name = raise NotImplementedError end class Circle include Drawable def initialize(radius) = @radius = radius def draw = puts "Drawing circle r=#{@radius}" def shape_name = "circle" end Circle.new(5).draw
import std.stdio; interface Drawable { void draw(); string shapeName(); } class Circle : Drawable { double radius; this(double r) { radius = r; } void draw() { writefln("Drawing circle r=%.1f", radius); } string shapeName() { return "circle"; } } void main() { Drawable shape = new Circle(5.0); shape.draw(); writeln(shape.shapeName()); }

D interfaces declare method signatures without implementations — equivalent to Ruby modules used purely as abstract interfaces. A class implements an interface by using the same : syntax as inheritance. A class can implement multiple interfaces but inherit from only one class. Unlike Ruby modules, D interfaces cannot contain state or default implementations (for that, use abstract classes).

Mixin templates (shared behavior)
module Greetable def greet = puts "Hello, I am #{name}" def farewell = puts "Goodbye from #{name}" end class Person include Greetable attr_reader :name def initialize(name) = @name = name end Person.new("Alice").greet
import std.stdio; mixin template Greetable() { void greet() { writefln("Hello, I am %s", name); } void farewell() { writefln("Goodbye from %s", name); } } class Person { string name; mixin Greetable; this(string n) { name = n; } } void main() { new Person("Alice").greet(); new Person("Bob").farewell(); }

D's mixin template injects code at the point of use — the closest equivalent to Ruby's module / include. The mixin has access to the including class's fields and methods (it becomes part of the class). Unlike Ruby modules which remain separate in the method lookup chain, D mixins are textually inlined — closer to Ruby's prepend or a language-level copy-paste.

alias this (implicit conversion)
# Ruby: define to_str or to_int for implicit conversion class Celsius def initialize(degrees) = @degrees = degrees def to_f = @degrees.to_f def +(other) = Celsius.new(@degrees + other.to_f) end
import std.stdio; struct Celsius { double degrees; alias degrees this; } void printTemp(double temp) { writefln("%.1f degrees", temp); } void main() { auto temp = Celsius(100.0); printTemp(temp); writeln(temp + 5.0); }

alias this designates a field (or method) as the implicit conversion target — when a Celsius is used where a double is expected, D automatically uses the degrees field. This is analogous to Ruby's to_str / to_int implicit conversion protocol but works at compile time and applies to all types uniformly, not just a special few methods.

Error Handling
try / catch / finally
begin raise "something went wrong" rescue RuntimeError => error puts "Caught: #{error.message}" ensure puts "Always runs" end
import std.stdio; void main() { try { throw new Exception("something went wrong"); } catch (Exception error) { writeln("Caught: ", error.msg); } finally { writeln("Always runs"); } }

D's try/catch/finally maps directly to Ruby's begin/rescue/ensure. The exception is accessed via a typed variable — catch (Exception error) — rather than Ruby's rescue => error. error.msg holds the message string. D also has Error (for unrecoverable errors like AssertError) which is distinct from Exception (for recoverable errors).

Exception hierarchy
begin [1,2,3].fetch(10) rescue IndexError => error puts "Index error: #{error.message}" end begin Integer("abc") rescue ArgumentError => error puts "Argument error: #{error.message}" end
import std.stdio; void main() { try { int[] numbers = [1, 2, 3]; writeln(numbers[10]); } catch (core.exception.RangeError error) { writeln("Range error: ", error.msg); } try { import std.conv; to!int("abc"); } catch (Exception error) { writeln("Conv error: ", error.msg); } }

D's exception hierarchy has Throwable at the root, with Error (fatal, like out-of-bounds) and Exception (recoverable) as the two branches. Array out-of-bounds throws core.exception.RangeError (an Error, not an Exception). In practice, catch Exception for recoverable errors and let Errors propagate — the same philosophy as Java's checked exceptions.

enforce — compact error checking
def divide(numerator, denominator) raise ArgumentError, "denominator cannot be zero" if denominator == 0 numerator.to_f / denominator end begin puts divide(10, 0) rescue ArgumentError => error puts error.message end
import std.stdio; import std.exception; double divide(double numerator, double denominator) { enforce(denominator != 0, "denominator cannot be zero"); return numerator / denominator; } void main() { try { writeln(divide(10.0, 0.0)); } catch (Exception error) { writeln(error.msg); } writeln(divide(10.0, 4.0)); }

enforce(condition, message) from std.exception throws an Exception if the condition is false — a compact replacement for if (!cond) throw new Exception(msg). This is equivalent to Ruby's guard clauses (raise ... unless condition) but as a function call. enforceEx!MyException(cond, msg) throws a specific exception type.

scope(failure) — error-path cleanup
def risky_operation puts "Starting" raise "oops" rescue => error puts "Error: #{error.message}" ensure puts "Cleanup runs either way" end risky_operation
import std.stdio; void riskyOperation() { writeln("Starting"); scope(failure) writeln("Error path cleanup"); scope(exit) writeln("Cleanup runs either way"); throw new Exception("oops"); } void main() { try { riskyOperation(); } catch (Exception error) { writeln("Caught: ", error.msg); } }

scope(failure) runs only when an exception propagates out of the scope — it is placed at the resource acquisition site and handles only the error path, unlike ensure which always runs. This makes intent clearer: scope(exit) = "always clean up", scope(failure) = "clean up on error only", scope(success) = "post-process on success". All three can coexist in the same scope.

Contract programming (in / out)
def factorial(n) raise ArgumentError, "n must be >= 0" if n < 0 result = (1..n).reduce(1, :*) raise "result must be positive" unless result > 0 result end puts factorial(5)
import std.stdio; long factorial(long n) in (n >= 0, "n must be non-negative") out (result; result > 0, "result must be positive") { long result = 1; foreach (i; 2..n+1) result *= i; return result; } void main() { writeln(factorial(5)); writeln(factorial(0)); }

D's contract programming is a first-class language feature: in blocks declare preconditions (what must be true when the function is called), out(result; ...) declares postconditions (what must be true about the return value). Contracts are checked in debug builds (dmd default) and elided in release builds (dmd -release). This is design-by-contract from Eiffel, built into the language — not a testing framework.

Templates & CTFE
Function templates (generics)
# Ruby uses duck typing — no generic syntax needed def maximum(a, b) = a > b ? a : b puts maximum(3, 7) puts maximum(3.14, 2.71) puts maximum("apple", "banana")
import std.stdio; T maximum(T)(T a, T b) { return a > b ? a : b; } void main() { writeln(maximum(3, 7)); writeln(maximum(3.14, 2.71)); writeln(maximum("apple", "banana")); }

D's template syntax T maximum(T)(T a, T b) reads as "a function named maximum with type parameter T, taking two T arguments". The compiler generates a specialized version for each type used. Unlike Ruby's duck typing (resolved at runtime), D's templates are monomorphized at compile time — every type combination gets its own optimized machine code.

Generic structs
# Ruby: duck typing handles any element type class Stack def initialize = @items = [] def push(item) = @items.push(item) def pop = @items.pop def size = @items.size end stack = Stack.new stack.push(1) stack.push(2) puts stack.pop
import std.stdio; struct Stack(T) { private T[] items; void push(T item) { items ~= item; } T pop() { auto top = items[$-1]; items = items[0..$-1]; return top; } size_t size() { return items.length; } } void main() { Stack!int intStack; intStack.push(1); intStack.push(2); writeln(intStack.pop()); writeln(intStack.size()); }

Generic structs use the same (T) template parameter syntax as functions. Stack!int instantiates the template for int — the ! is D's template argument operator. size_t is the platform-native unsigned integer type (like C's size_t), appropriate for sizes and array indices. Attempting Stack!int.push("hello") is a compile error.

Compile-Time Function Execution (CTFE)
# Ruby: computed at runtime every time def factorial(n) = n <= 1 ? 1 : n * factorial(n - 1) MAX_FACTORIAL = factorial(10) puts MAX_FACTORIAL
import std.stdio; pure long factorial(long n) { return n <= 1 ? 1 : n * factorial(n - 1); } enum FACT_10 = factorial(10); void main() { writeln(FACT_10); writeln(typeof(FACT_10).stringof); static assert(FACT_10 == 3_628_800); }

D's enum with a value expression runs the right-hand side at compile time — this is CTFE. Any pure function can be used in a enum or static context and will execute during compilation. The result is baked into the binary as a literal constant. static assert verifies the value at compile time — if the assertion fails, it is a compile error, not a runtime error. Ruby constants are evaluated once at parse time but are not true compile-time constants.

static if — compile-time conditionals
# Ruby: runtime type checking def describe(value) if value.is_a?(Integer) puts "integer: #{value}" elsif value.is_a?(String) puts "string of length #{value.length}" end end describe(42) describe("hello")
import std.stdio; void describe(T)(T value) { static if (is(T == int) || is(T == long)) writefln("integer: %d", value); else static if (is(T == string)) writefln("string of length %d", value.length); else writefln("other: %s", value); } void main() { describe(42); describe("hello"); describe(3.14); }

static if evaluates its condition at compile time and includes only the matching branch in the compiled code — the other branches are not compiled at all. This is how D implements conditional compilation without preprocessor macros. When combined with templates, static if enables compile-time polymorphism: the compiler generates different code for each type, with zero runtime overhead for the type dispatch.

Built-in unittest blocks
require "minitest/autorun" def square(x) = x ** 2 class TestSquare < Minitest::Test def test_square assert_equal 9, square(3) assert_equal 0, square(0) assert_equal 16, square(-4) end end
import std.stdio; int square(int x) { return x * x; } unittest { assert(square(3) == 9); assert(square(0) == 0); assert(square(-4) == 16); } void main() { writeln("All tests passed!"); writeln(square(5)); }

unittest blocks are a built-in D language feature — no test framework import required. They live next to the code they test and run automatically before main() when compiled with -unittest (which this runner does). If any assertion fails, the program aborts before reaching main(). This zero-friction testing model encourages inline tests, similar to Rust's #[test] attribute.