Basics & Running
Hello, World
void main() {
print('Hello, World!');
} puts "Hello, World!" Ruby scripts run top to bottom with no entry function —
puts "Hello, World!" is a complete program. There are no semicolons, no type declarations, and parentheses on a method call are optional. puts prints with a trailing newline; p prints a value's inspected form (useful for debugging), and print omits the newline.Dynamic, duck typing
The whole basis shifts. Dart checks types at compile time; Ruby has no compiler and no type annotations — a value's suitability is decided by whether it responds to the method you call.
String describe(Object value) => 'got ${value.runtimeType}';
void main() {
print(describe(42));
print(describe('hi'));
} def describe(value)
"got #{value.class}"
end
puts describe(42)
puts describe("hi")
puts describe([1, 2]) # any object at all — no type to declare A Ruby method declares no parameter or return types, and
describe accepts literally any object. This is duck typing: if a value responds to the methods you use, it works; if it does not, you get a NoMethodError at the moment that line runs — not at compile time, because there is no compile time. The freedom is enormous and the safety net Dart gives you is gone.Implicit return
int square(int n) => n * n; // arrow body returns
int cube(int n) {
return n * n * n; // explicit return
}
void main() {
print(square(5));
print(cube(3));
} def square(n)
n * n # the last expression is the return value
end
def cube(n) = n * n * n # Ruby 4.0 endless method, like Dart's =>
puts square(5)
puts cube(3) Every Ruby method returns the value of its last evaluated expression, so an explicit
return is rare (reserved for early exits). Ruby 4.0's one-liner "endless method" def cube(n) = … is the near-twin of Dart's arrow body. Because almost everything is an expression — if, case, and blocks all return values — this implicit-return style reaches much further than in Dart.String interpolation
void main() {
final name = 'Ada';
final year = 1843;
print('$name wrote code in $year.');
print('Next: ${year + 1}');
} name = "Ada"
year = 1843
puts "#{name} wrote code in #{year}."
puts "Next: #{year + 1}" Ruby interpolates with
#{…} inside double-quoted strings — one form for both a variable and a full expression, where Dart offers $name and ${…}. Single-quoted Ruby strings do no interpolation and no escape processing (except \\ and \'), so they are the literal-text choice.Everything is an Object
Numbers are objects too
void main() {
for (var i = 0; i < 3; i++) {
print('tick');
}
print(5.abs());
} 3.times { puts "tick" } # Integer#times takes a block
puts(-5.abs) # methods on numbers
puts 10.gcd(15) # 5
puts 2.between?(1, 3) # true In Ruby there are no primitives:
3 is an Integer object, and 3.times { … } calls a method on it. Even operators are methods — 1 + 2 is 1.+(2) — which is why you can define + on your own classes (shown later). This uniform "everything is an object receiving messages" model has no Dart equivalent, where numbers behave like values with special syntax.nil is an object
void main() {
int? missing;
// print(missing.toString()); // won't compile: missing may be null
print(missing?.toString() ?? 'absent');
} missing = nil
puts missing.class # NilClass — nil is a real object
puts missing.to_s.empty? # "" — nil responds to to_s
puts missing.to_a.inspect # [] — and to_a
puts missing.nil? # true Where Dart's
null is a tracked absence the compiler reasons about, Ruby's nil is a singleton object (the sole instance of NilClass) with real methods: nil.to_s is "", nil.to_a is []. That makes many nil values harmless in practice — but calling a method nil does not have still raises NoMethodError at run time, which the next section guards against.nil & Truthiness
No null safety; &. and ||
String? findName(int id) => id == 1 ? 'Ada' : null;
void main() {
final name = findName(2);
print(name?.toUpperCase() ?? 'anonymous'); // ?. and ??
} def find_name(id) = id == 1 ? "Ada" : nil
name = find_name(2)
puts name&.upcase || "anonymous" # &. is ?. ; || is (roughly) ??
count = nil
count ||= 5 # ||= assigns only if nil/false
puts count Ruby has no compile-time null safety — nothing stops you from calling a method on a value that turns out to be
nil. The runtime tools rhyme with Dart's: &. is the safe-navigation operator (?.), and ||/||= stand in for ??/??=. The catch is that || falls back on nil and false (and nothing else), so it is not a precise null check — which the truthiness rules below make important.Only nil and false are falsy
void main() {
// Dart demands an actual bool; 0 and '' are NOT booleans.
final count = 0;
if (count != 0) print('nonzero');
final text = '';
if (text.isNotEmpty) print('nonempty');
print('done');
} # In Ruby, ONLY nil and false are falsy — 0 and "" are truthy!
puts "zero is truthy" if 0
puts "empty is truthy" if ""
puts "nil is falsy" unless nil
count = 0
puts "count present" if count # prints — 0 does NOT mean "no" This is a classic trap for anyone from a stricter language. In Ruby every value is truthy except
nil and false — so 0, "", and [] are all true. Dart, by contrast, requires a genuine bool in a condition and would reject if (count). Guard for emptiness explicitly (count.zero?, text.empty?) rather than leaning on truthiness the way you might elsewhere.Variables & Symbols
Variables & constants by convention
void main() {
final maxRetries = 3; // final = cannot reassign
var counter = 0;
counter += 1;
print('$maxRetries $counter');
} MAX_RETRIES = 3 # a CONSTANT: capitalized name, by convention
counter = 0 # a local variable: lowercase
counter += 1
MAX_RETRIES = 4 # only WARNS, does not error — Ruby trusts you
puts "#{MAX_RETRIES} #{counter}" Ruby has no
final/var distinction. A name's role comes from its case: MAX_RETRIES is a constant because it is capitalized, a lowercase name is a local variable. Reassigning a constant only prints a warning rather than failing, so the immutability is a convention the compiler cannot enforce — very different from Dart's checked final. Prefixes also carry meaning: @name is an instance variable, $name a global.Symbols
void main() {
// Dart has no symbol type; you use strings or enums as identifiers.
const status = 'active';
final labels = {'active': 'On', 'inactive': 'Off'};
print(labels[status]);
} status = :active # a Symbol, not a string
labels = { active: "On", inactive: "Off" } # symbol keys
puts labels[status]
puts :active.equal?(:active) # true — same object every time
puts "active".equal?("active") # false — two string objects A Symbol (
:active) is an immutable, interned identifier with no Dart counterpart — the same symbol is always the exact same object, which makes symbols the idiomatic choice for hash keys, method names, and enum-like constants. They are cheaper and more intention-revealing than strings for those roles, so { active: "On" } (symbol keys) is far more common in Ruby than string keys.Strings
Strings are frozen by default
void main() {
// Dart strings are immutable — there is no in-place mutation at all.
var greeting = 'hello';
greeting = greeting.toUpperCase(); // rebinds to a new string
print(greeting);
} greeting = "hello"
# greeting << " world" # FrozenError in Ruby 4.0 — literals are frozen
mutable = +"hello" # unary + gives an unfrozen copy
mutable << " world" # now this works
puts mutable
puts greeting.upcase # non-mutating methods always return a new string As of Ruby 4.0, string literals are frozen by default, so
"hello" << " world" raises a FrozenError. This narrows a historical gap with Dart, whose strings are fully immutable — but Ruby still allows mutable strings on request via the unary + (+"hello") or .dup, and mutating methods like <<, gsub!, and upcase! exist for them. Non-bang methods (upcase) always return a fresh string.Bang methods mutate
void main() {
final words = ['banana', 'apple', 'cherry'];
final sorted = [...words]..sort(); // copy, then sort in place
print(words); // unchanged
print(sorted); // sorted
} words = ["banana", "apple", "cherry"]
sorted = words.sort # returns a NEW sorted array
puts words.inspect # unchanged
puts sorted.inspect # sorted
words.sort! # the bang version sorts IN PLACE
puts words.inspect # now the original is sorted Ruby's naming convention pairs a safe method with a mutating twin marked by a
!: sort returns a new array, sort! reorders the receiver in place and returns it. The ! is a "danger, this mutates" flag (also seen on map!, reject!, upcase!). Dart has no such convention — you copy explicitly with the spread operator and cascade, as on the left.Arrays & Hashes
List → Array
void main() {
final numbers = <int>[1, 2, 3];
numbers.add(4);
print(numbers.length);
print(numbers.first);
print(numbers.contains(2));
} numbers = [1, 2, 3]
numbers << 4 # << is the idiomatic "append"
puts numbers.length
puts numbers.first
puts numbers.include?(2) # predicates end in ?
puts numbers.last(2).inspect A Dart
List is a Ruby Array, untyped and heterogeneous — [1, "two", :three] is fine. Appending is idiomatically << (though push exists), and Ruby's convention is that boolean-returning methods end in ? (include?, empty?, any?). The Array API is vast and mostly comes from the Enumerable module, covered next.Map → Hash
void main() {
final ages = {'Ada': 36, 'Grace': 45};
print(ages['Ada']);
print(ages['Nobody']); // null
ages.forEach((name, age) => print('$name: $age'));
} ages = { "Ada" => 36, "Grace" => 45 } # => for string keys
puts ages["Ada"]
puts ages["Nobody"].inspect # nil for a missing key
ages.each { |name, age| puts "#{name}: #{age}" }
symbols = { ada: 36, grace: 45 } # shorthand for symbol keys
puts symbols[:ada] A Ruby
Hash is Dart's Map, written with => between key and value ("Ada" => 36), or the key: value shorthand when keys are symbols. A missing key returns nil like Dart, but you can set a default (Hash.new(0)) so lookups never return nil — handy for counting. Iteration uses a block, each { |k, v| … }, rather than a callback.Enumerable: map/select/reduce
void main() {
final numbers = [1, 2, 3, 4, 5, 6];
final result = numbers
.where((n) => n.isEven)
.map((n) => n * n)
.fold(0, (sum, n) => sum + n);
print(result); // 56
} numbers = [1, 2, 3, 4, 5, 6]
result = numbers
.select { |n| n.even? } # where -> select (or filter)
.map { |n| n * n }
.sum # dedicated sum; or .reduce(0, :+)
puts result # 56 Ruby's
Enumerable is the richest collection library on this site, and the chain maps closely: where is select (aliased filter), map is map, and fold is reduce/inject — though dedicated methods like sum, min, group_by, partition, and each_with_object usually read better. Every one takes a block, the syntax explored next.Blocks, Procs & Lambdas
Blocks & yield
Blocks are the feature that most defines Ruby. A block is a chunk of code passed to a method, and the method runs it with
yield — the mechanism behind each, map, and resource-management methods alike.void repeat(int times, void Function(int) action) {
for (var i = 0; i < times; i++) action(i);
}
void main() {
repeat(3, (i) => print('line $i'));
} def repeat(times)
i = 0
while i < times
yield i # run the block passed to this method
i += 1
end
end
repeat(3) { |i| puts "line #{i}" } # the block is the { … } after the call A Ruby method can take one block — written
{ |x| … } for one line or do |x| … end for many — without declaring it as a parameter, and invokes it with yield (or checks block_given? first). Where Dart passes a closure as an explicit argument, Ruby gives blocks their own syntax, which is why File.open(path) { |file| … } reads so cleanly: the method opens, yields, and closes around your block.Procs & lambdas
void main() {
final square = (int n) => n * n; // a first-class function value
print(square(5));
final apply = (int x, int Function(int) fn) => fn(x);
print(apply(4, square));
} square = ->(n) { n * n } # a lambda (stabby lambda syntax)
puts square.call(5) # .call, .() or square[5]
puts square.(5)
double = proc { |n| n * 2 } # a Proc — looser about arity than a lambda
puts double.call(21) When you need to store a block as a value, Ruby gives you two closures: a lambda (
->(n) { … }) and a Proc (proc { … }). They are Dart's function values, invoked with .call (or .()). The differences are Ruby-specific: a lambda checks its argument count and returns only from itself, while a Proc is lax about arity and returns from the enclosing method — subtleties Dart's uniform closures never present.Symbol#to_proc (&:method)
void main() {
final words = ['ada', 'grace', 'alan'];
final upper = words.map((word) => word.toUpperCase()).toList();
print(upper);
} words = ["ada", "grace", "alan"]
upper = words.map(&:upcase) # &:upcase == { |w| w.upcase }
puts upper.inspect
lengths = words.map(&:length)
puts lengths.inspect The
&:upcase idiom is pure Ruby shorthand: & converts the symbol :upcase into a block that calls that method on each element, so map(&:upcase) means map { |w| w.upcase }. It is the terse form you will see everywhere for "call this one method on everything," with no direct Dart equivalent — Dart requires the full (word) => word.toUpperCase().Methods & Arguments
Named params → keyword arguments
String connect(String host, {int port = 5432, bool tls = true}) =>
'$host:$port tls=$tls';
void main() {
print(connect('db.example'));
print(connect('db.example', port: 6432, tls: false));
} def connect(host, port: 5432, tls: true)
"#{host}:#{port} tls=#{tls}"
end
puts connect("db.example")
puts connect("db.example", port: 6432, tls: false) This maps almost exactly. Ruby's keyword arguments —
def connect(host, port: 5432, tls: true) — are the counterpart of Dart's named parameters with defaults, passed the same way at the call site (port: 6432). A keyword without a default (port:) is required, mirroring Dart's required. Ruby also has positional defaults and a *splat for variadics, covered next.Variadic splat & multiple assignment
int sumAll(List<int> numbers) =>
numbers.fold(0, (total, n) => total + n);
void main() {
print(sumAll([1, 2, 3, 4]));
final (a, b) = (1, 2); // record destructure
print('$a $b');
} def sum_all(*numbers) # * gathers args into an array
numbers.sum
end
puts sum_all(1, 2, 3, 4) # call with separate args
first, *rest = [1, 2, 3, 4] # * also destructures
puts "#{first} #{rest.inspect}"
a, b = 1, 2 # parallel assignment
puts "#{a} #{b}" The
* splat operator collects trailing arguments into an array (def sum_all(*numbers)), so callers pass separate values with no list literal — Dart cannot do this. The same * destructures on assignment (first, *rest = …), and Ruby's parallel assignment a, b = 1, 2 covers what Dart records and their patterns do — including the one-line swap a, b = b, a.Control Flow & Patterns
if, unless & modifiers
void main() {
final score = 72;
if (score >= 60) {
print('pass');
}
final grade = score >= 90 ? 'A' : 'B'; // ternary
print(grade);
} score = 72
puts "pass" if score >= 60 # modifier if — reads as a sentence
puts "low" unless score >= 60 # unless == "if not"
grade = score >= 90 ? "A" : "B" # Ruby has the ternary too
puts grade
# if is an expression: the whole thing returns a value
label = if score >= 60 then "ok" else "no" end
puts label Ruby keeps the ternary, but adds two things Dart lacks: the modifier form (
puts "pass" if score >= 60) that trails the condition after the statement, and unless as a first-class "if not." Crucially, if is an expression that returns a value, so you can assign its result — one reason explicit returns are so rare in Ruby.case/when with ===
String classify(Object value) => switch (value) {
int() => 'number',
String() => 'text',
_ => 'other',
};
void main() {
print(classify(42));
print(classify('hi'));
} def classify(value)
case value
when Integer then "number" # uses Integer === value
when String then "text"
when 1..10 then "small" # ranges work via Range#===
else "other"
end
end
puts classify(42)
puts classify("hi") Ruby's
case/when matches with the case-equality operator ===, which each class defines: Integer === 42, (1..10) === 5, /ab/ === "abc". That makes a single case dispatch on type, range, or regex — more flexible than Dart's value switch, though Dart 3's type patterns cover the type case. Like if, case returns a value.Pattern matching (case/in)
void main() {
final point = (2, 0);
final result = switch (point) {
(0, 0) => 'origin',
(final x, 0) => 'on x-axis at $x',
_ => 'elsewhere',
};
print(result);
} config = { name: "web", port: 8080 }
case config
in { name: String => name, port: Integer => port }
puts "#{name} on #{port}" # destructure AND bind
in { name: }
puts "just #{name}"
else
puts "unknown"
end Ruby 3 added structural pattern matching with
case/in, the close cousin of Dart 3 patterns — it destructures arrays and hashes, binds variables, and checks types in one step (port: Integer => port). Note the two different constructs: case/when (above) tests with === and does not bind, while case/in matches structure and binds, the way Dart's single switch does both.Ranges & iteration
void main() {
for (var i = 1; i <= 3; i++) print(i);
for (final color in ['red', 'green']) print(color);
} (1..3).each { |i| puts i } # inclusive range 1,2,3
(1...3).each { |i| puts i } # exclusive range 1,2
3.times { |i| puts i } # 0,1,2
["red", "green"].each { |c| puts c }
puts (1..5).to_a.inspect # ranges are objects: [1,2,3,4,5] Ruby has range literals —
1..3 (inclusive) and 1...3 (exclusive) — that Dart lacks, and they are objects you can iterate, convert (to_a), or match in a case. Iteration is done by sending each (or times, upto) a block rather than writing a C-style for; the bare for keyword exists but is almost never used in idiomatic Ruby.Classes & Objects
Classes & initialize
class Point {
final int x;
final int y;
Point(this.x, this.y);
int manhattan() => x.abs() + y.abs();
}
void main() {
print(Point(3, -4).manhattan());
} class Point
def initialize(x, y) # the constructor is named initialize
@x = x # @x is an instance variable
@y = y
end
def manhattan
@x.abs + @y.abs
end
end
puts Point.new(3, -4).manhattan A Ruby class's constructor is the method
initialize, called for you by Point.new. Instance state lives in @-prefixed variables (@x), which are private to the object and not declared up front — you simply assign them. There are no type annotations and no this keyword (the receiver is self, usually implicit), so a class body is strikingly terse next to its Dart equivalent.Getters/setters → attr_accessor
class Person {
String name;
int age;
Person(this.name, this.age);
}
void main() {
final person = Person('Ada', 36);
person.age = 37;
print('${person.name} ${person.age}');
} class Person
attr_accessor :name, :age # generates reader AND writer methods
def initialize(name, age)
@name = name
@age = age
end
end
person = Person.new("Ada", 36)
person.age = 37 # calls the generated age= method
puts "#{person.name} #{person.age}" Instance variables are private in Ruby, so exposing them needs accessor methods — and
attr_accessor :name, :age generates both the reader and the name=/age= writer for you (with attr_reader/attr_writer for one direction). It is the counterpart of Dart's public fields and getters/setters, but note person.age = 37 is actually a method call to age=, which is why you can add validation later without changing callers.Operator overloading
class Vector {
final int x, y;
const Vector(this.x, this.y);
Vector operator +(Vector other) => Vector(x + other.x, y + other.y);
@override
String toString() => '($x, $y)';
}
void main() {
print(Vector(1, 2) + Vector(3, 4));
} class Vector
attr_reader :x, :y
def initialize(x, y) = (@x, @y = x, y)
def +(other) # operators are just methods named + etc.
Vector.new(@x + other.x, @y + other.y)
end
def to_s = "(#{@x}, #{@y})" # overriding to_s, like Dart's toString
end
puts Vector.new(1, 2) + Vector.new(3, 4) Because operators are ordinary methods, you overload one by defining a method named after it:
def +(other), and likewise ==, <=> (the "spaceship," which powers sorting), and []. Overriding to_s customizes string conversion the way Dart's toString does. This is the same capability as Dart's operator +, just falling naturally out of "everything is a method."Modules, Mixins & Open Classes
Mixins → module include
mixin Greetable {
String get who;
String greet() => 'Hello, $who';
}
class Person with Greetable {
@override
final String who;
Person(this.who);
}
void main() => print(Person('Ada').greet()); module Greetable
def greet = "Hello, #{who}" # relies on the host providing who
end
class Person
include Greetable # mixes the module's methods in
attr_reader :who
def initialize(who) = (@who = who)
end
puts Person.new("Ada").greet Ruby's mixins come from modules: a
module holds shared methods, and a class pulls them in with include (adding instance methods) or extend (adding class methods). It is the direct ancestor of Dart's with mixins — Dart borrowed the idea — but Ruby modules also serve as namespaces and, via Comparable/Enumerable, as the standard way to get a whole API from one required method.Open classes (monkey patching)
extension NumberWords on int {
String get spelledOut => this == 1 ? 'one' : 'many';
}
void main() {
print(3.spelledOut); // scoped extension method
} class Integer # REOPEN the built-in Integer class
def spelled_out = self == 1 ? "one" : "many"
end
puts 3.spelled_out # the method now exists on every integer, globally Where Dart's extension methods are scoped and resolved statically, Ruby lets you reopen any class — even
Integer or String — and add or replace methods everywhere, permanently, at run time. This "monkey patching" is enormously powerful (it is how libraries like Rails add 3.days.ago) and correspondingly dangerous: there is no scoping, so a patch affects the entire program. Ruby hands you the keys; use them sparingly.Duck typing, not interfaces
abstract class Renderable {
String render();
}
class Button implements Renderable {
@override
String render() => '[Button]';
}
String show(Renderable item) => item.render();
void main() => print(show(Button())); class Button
def render = "[Button]" # no interface declared or implemented
end
def show(item)
item.render # works for ANY object that has #render
end
puts show(Button.new)
puts respond_to_check = Button.new.respond_to?(:render) # true Ruby has no
interface or implements. If an object responds to render, it can be passed to show — that is duck typing, and conformance is never declared. You can ask an object respond_to?(:render) at run time if you must check, but the idiom is simply to call the method and let a NoMethodError surface the mistake. It is the polar opposite of Dart's compile-time interface checking.Metaprogramming
send & define_method
Ruby can call methods by name and define new ones while the program runs — a level of runtime reflection Dart's static model deliberately does not offer.
void main() {
final text = 'hello';
// Dart cannot invoke a method chosen by a runtime string, nor define
// methods at run time — the method set is fixed at compile time.
print(text.toUpperCase());
} text = "hello"
name = :upcase
puts text.send(name) # call a method chosen at run time
class Widget
[:show, :hide].each do |action|
define_method(action) { "#{action}!" } # generate methods in a loop
end
end
puts Widget.new.show
puts Widget.new.hide Two everyday metaprogramming tools.
send(:upcase) invokes a method whose name you hold as a symbol or string — dynamic dispatch chosen at run time — and define_method creates methods programmatically, so the two Widget methods here are written by a loop rather than by hand. Dart has no equivalent: its method set is fixed at compile time, which is safer but far less malleable.method_missing
void main() {
// Dart raises NoSuchMethodError and there is no ergonomic hook to
// intercept arbitrary unknown calls at run time.
final map = {'name': 'Ada'};
print(map['name']);
} class OpenStruct2
def initialize = (@data = {})
def method_missing(name, *args)
key = name.to_s
if key.end_with?("=") # a setter call like name=
@data[key.chomp("=")] = args.first
else
@data[key] # a getter call
end
end
end
obj = OpenStruct2.new
obj.name = "Ada" # no such method — intercepted
puts obj.name When an object receives a message it has no method for, Ruby calls
method_missing before failing — letting you intercept arbitrary calls and respond dynamically. Here it fakes name/name= accessors that were never defined. This is how libraries build fluent, schema-free APIs (record.any_column_name), and it has no counterpart in Dart, where an unknown method is simply a NoSuchMethodError.Exceptions
try/catch → begin/rescue
void main() {
try {
throw FormatException('bad input');
} on FormatException catch (error) {
print('caught: ${error.message}');
} finally {
print('done');
}
} begin
raise ArgumentError, "bad input" # raise, not throw
rescue ArgumentError => error # rescue by class, bind with =>
puts "caught: #{error.message}"
ensure
puts "done" # ensure == finally
end The keywords rename but the structure is familiar:
raise throws, rescue ClassName => error catches by exception class (Dart's on Type catch), and ensure is finally. Inside a method you can drop the begin/end and put rescue in the method body directly. Ruby also has retry to re-run the begin block — useful for transient failures, with no Dart equivalent.Custom exceptions
class InsufficientFunds implements Exception {
final int shortfall;
InsufficientFunds(this.shortfall);
@override
String toString() => 'Short by $shortfall cents';
}
void main() {
try {
throw InsufficientFunds(150);
} catch (error) {
print(error);
}
} class InsufficientFunds < StandardError # inherit from StandardError
def initialize(shortfall)
@shortfall = shortfall
super("Short by #{shortfall} cents")
end
attr_reader :shortfall
end
begin
raise InsufficientFunds.new(150)
rescue InsufficientFunds => error
puts error.message
puts error.shortfall
end A custom Ruby exception is a class that inherits from
StandardError (the base you should rescue, not the broader Exception). Calling super("…") sets the message that error.message returns. It parallels Dart's "implement Exception and override toString," but through ordinary inheritance — and the convention of rescuing StandardError rather than everything is worth internalizing.Requires & Gems
Libraries → require
void main() {
// Dart: import 'dart:math'; then use sqrt(2), pi, etc.
print('imports are per-file and resolved by path');
} require "set" # load a standard library
seen = Set.new([1, 2, 2, 3])
seen << 3
puts seen.size # 3
puts seen.include?(2) # true Ruby loads code with
require (for installed libraries and gems) or require_relative (for files by relative path), executed as a statement at run time rather than resolved statically like Dart's import. A required library's names land in the global namespace unless it defines a module to scope them — there is no per-import prefix like Dart's import … as math by default.pub → gems & Bundler
// pubspec.yaml
// dependencies:
// http: ^1.2.0
//
// Then: dart pub get
// Then: import 'package:http/http.dart' as http;
void main() {
print('Dependencies are declared in pubspec.yaml and fetched by pub.');
} # Gemfile
# source "https://rubygems.org"
# gem "sinatra", "~> 4.0"
#
# Then: bundle install
# Then in code: require "sinatra"
puts "Dependencies live in a Gemfile and are installed by Bundler." Ruby packages are gems, declared in a
Gemfile and installed by Bundler (bundle install) — the direct analog of pubspec.yaml plus dart pub get. Gems come from rubygems.org, and the version-constraint syntax rhymes: Ruby's pessimistic operator ~> 4.0 plays the role of Dart's caret ^1.2.0, allowing compatible updates but not breaking ones.