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.
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.
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.
# 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.
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.
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.
# 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.
# 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.
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.
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.
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.
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].
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.
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?.
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.
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.
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.
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.
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.
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.
# 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
(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.
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.
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).
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).
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).
# 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.
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.
# 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.
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.
# 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.
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.
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).
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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).
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.
# 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.
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).
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.
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.
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.
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.
# 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.
# 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.
# 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.
# 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.
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.