PONY λ M2 Modula-2

Dart.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Dart and Ruby

Dart 3.7 Ruby 4.0
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 argumentsdef 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.